diff --git a/WireAPI/Package.swift b/WireAPI/Package.swift index ef87437be80..7faf64bd428 100644 --- a/WireAPI/Package.swift +++ b/WireAPI/Package.swift @@ -33,6 +33,7 @@ let package = Package( "WireAPI", "WireAPISupport", .product(name: "WireTestingPackage", package: "WireFoundation"), + .product(name: "WireFoundationSupport", package: "WireFoundation"), .product(name: "SnapshotTesting", package: "swift-snapshot-testing") ], resources: [ @@ -47,7 +48,8 @@ let package = Package( .process("APIs/UserPropertiesAPI/Resources"), .process("APIs/SelfUserAPI/Resources"), .process("APIs/UserClientsAPI/Resources"), - .process("Network/PushChannel/Resources") + .process("Network/PushChannel/Resources"), + .process("Authentication/Resources") ] ) ] diff --git a/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPI.swift b/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPI.swift index 0c48100968c..89a00f78de6 100644 --- a/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPI.swift +++ b/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPI.swift @@ -24,6 +24,6 @@ public protocol PushChannelAPI { /// - Parameter clientID: The id of the self client. /// - Returns: A push channel. - func createPushChannel(clientID: String) throws -> any PushChannelProtocol + func createPushChannel(clientID: String) async throws -> any PushChannelProtocol } diff --git a/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPIImpl.swift b/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPIImpl.swift index a8151bb0be5..edd3788791a 100644 --- a/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPIImpl.swift +++ b/WireAPI/Sources/WireAPI/APIs/PushChannelAPI/PushChannelAPIImpl.swift @@ -26,7 +26,7 @@ class PushChannelAPIImpl: PushChannelAPI { self.pushChannelService = pushChannelService } - func createPushChannel(clientID: String) throws -> any PushChannelProtocol { + func createPushChannel(clientID: String) async throws -> any PushChannelProtocol { var components = URLComponents(string: "/await") components?.queryItems = [URLQueryItem(name: "client", value: clientID)] @@ -38,7 +38,7 @@ class PushChannelAPIImpl: PushChannelAPI { .withMethod(.get) .build() - return try pushChannelService.createPushChannel(request) + return try await pushChannelService.createPushChannel(request) } } diff --git a/WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift b/WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift new file mode 100644 index 00000000000..98e24c933ae --- /dev/null +++ b/WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift @@ -0,0 +1,172 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireFoundation + +// sourcery: AutoMockable +protocol AuthenticationManagerProtocol { + + func getValidAccessToken() async throws -> AccessToken + func refreshAccessToken() async throws -> AccessToken + +} + +actor AuthenticationManager: AuthenticationManagerProtocol { + + enum Failure: Error, Equatable { + + case invalidCredentials + + } + + private enum CurrentToken { + + case cached(AccessToken) + case renewing(Task) + + } + + private var currentToken: CurrentToken? + private let clientID: String + private let cookieStorage: any CookieStorageProtocol + private let networkService: NetworkService + + init( + clientID: String, + cookieStorage: any CookieStorageProtocol, + networkService: NetworkService + ) { + self.clientID = clientID + self.cookieStorage = cookieStorage + self.networkService = networkService + } + + /// Get a valid access token to make authenticated requests. + /// + /// If a valid token exists in the cache then it will be returned, + /// otherwise a new token will be retrieved from the backend. + /// + /// - Returns: A valid (non-expired) access token. + + func getValidAccessToken() async throws -> AccessToken { + switch currentToken { + case let .renewing(task): + // A new token will come soon, wait + try await task.value + + case let .cached(accessToken) where !accessToken.isExpiring: + // This one is still good. + accessToken + + default: + // Time for a new token. + try await refreshAccessToken() + } + } + + /// Get a new access token from the backend. + /// + /// This method will fetch a new access token from the backend + /// and then store it in the cache. Only a single request is made + /// at a time, and repeated calls will await the result of any + /// in-flight requests. + /// + /// - Returns: A new access token. + + func refreshAccessToken() async throws -> AccessToken { + if case let .renewing(task) = currentToken { + // A new token will come soon, wait + return try await task.value + } + + var lastKnownToken: AccessToken? + if case let .cached(token) = currentToken { + lastKnownToken = token + } + + let task = makeRenewTokenTask(lastKnownToken: lastKnownToken) + currentToken = .renewing(task) + + do { + let newToken = try await task.value + currentToken = .cached(newToken) + return newToken + } catch { + currentToken = nil + throw error + } + } + + private func makeRenewTokenTask( + lastKnownToken: AccessToken? + ) -> Task { + Task { + let cookies = try await cookieStorage.fetchCookies() + + var request = try URLRequestBuilder(path: "/access") + .withQueryItem(name: "client_id", value: clientID) + .withMethod(.post) + .withAcceptType(.json) + .withCookies(cookies) + .build() + + if let lastKnownToken { + request.setAccessToken(lastKnownToken) + } + + let (data, response) = try await networkService.executeRequest(request) + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + return try ResponseParser(decoder: decoder) + .success(code: .ok, type: AccessTokenPayload.self) + .failure(code: .forbidden, label: "invalid-credentials", error: Failure.invalidCredentials) + .parse(code: response.statusCode, data: data) + } + } + +} + +extension AccessToken { + + var isExpiring: Bool { + let secondsRemaining = expirationDate.timeIntervalSinceNow + return secondsRemaining < 40 + } + +} + +private struct AccessTokenPayload: Decodable, ToAPIModelConvertible { + + let user: UUID + let accessToken: String + let tokenType: String + let expiresIn: Int + + func toAPIModel() -> AccessToken { + AccessToken( + userID: user, + token: accessToken, + type: tokenType, + expirationDate: Date(timeIntervalSinceNow: TimeInterval(expiresIn)) + ) + } + +} diff --git a/WireAPI/Sources/WireAPI/Components/AuthenticationStorage/AuthenticationStorage.swift b/WireAPI/Sources/WireAPI/Authentication/AuthenticationStorage.swift similarity index 73% rename from WireAPI/Sources/WireAPI/Components/AuthenticationStorage/AuthenticationStorage.swift rename to WireAPI/Sources/WireAPI/Authentication/AuthenticationStorage.swift index c96e941cc18..1521306b5e3 100644 --- a/WireAPI/Sources/WireAPI/Components/AuthenticationStorage/AuthenticationStorage.swift +++ b/WireAPI/Sources/WireAPI/Authentication/AuthenticationStorage.swift @@ -26,24 +26,24 @@ public protocol AuthenticationStorage { /// /// - Parameter accessToken: The token to store. - func storeAccessToken(_ accessToken: AccessToken) + func storeAccessToken(_ accessToken: AccessToken) async /// Fetch a stored access token. /// /// - Returns: The stored access token. - func fetchAccessToken() -> AccessToken? + func fetchAccessToken() async -> AccessToken? - /// Store a cookie. + /// Store cookies. /// - /// - Parameter cookieData: The cookie data to store. + /// - Parameter cookies: The cookies to store. - func storeCookieData(_ cookieData: Data?) + func storeCookies(_ cookies: [HTTPCookie]) async throws - /// Fetch a stored cookie. + /// Fetch stored cookies. /// - /// - Returns: The stored cookie data. + /// - Returns: The stored cookies. - func fetchCookieData() -> Data? + func fetchCookies() async throws -> [HTTPCookie] } diff --git a/WireAPI/Sources/WireAPI/Authentication/CookieStorage.swift b/WireAPI/Sources/WireAPI/Authentication/CookieStorage.swift new file mode 100644 index 00000000000..13991733c14 --- /dev/null +++ b/WireAPI/Sources/WireAPI/Authentication/CookieStorage.swift @@ -0,0 +1,168 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireFoundation + +// sourcery: AutoMockable +protocol CookieStorageProtocol: Sendable { + + func storeCookies(_ cookies: [HTTPCookie]) async throws + func fetchCookies() async throws -> [HTTPCookie] + +} + +actor CookieStorage: CookieStorageProtocol { + + enum Failure: Error { + + case malformedCookieData + case failedToDecodeCookieData(any Error) + case missingCookieEncryptionKey + case failedToEncryptCookie(any Error) + case failedToDecryptCookie(any Error) + + } + + private let userID: UUID + private let cookieEncryptionKey: Data + private let keychain: any KeychainProtocol + + private lazy var baseQuery: Set = [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword) + ] + + private lazy var fetchQuery: Set = { + var result = baseQuery + result.insert(.returningData(true)) + return result + }() + + private func addQuery(cookieData: Data) -> Set { + var result = updateQuery(cookieData: cookieData) + result.insert(.accessible(.afterFirstUnlock)) + return result + } + + private func updateQuery(cookieData: Data) -> Set { + var result = baseQuery + result.insert(.data(cookieData.base64EncodedData())) + return result + } + + init( + userID: UUID, + cookieEncryptionKey: Data, + keychain: any KeychainProtocol + ) { + self.userID = userID + self.cookieEncryptionKey = cookieEncryptionKey + self.keychain = keychain + } + + /// Store cookies. + /// + /// Cookie data is stored in the device keychain and may persist across + /// different installations of the application, such as when the app is + /// deleted without the user logging out. + /// + /// - Parameter cookies: The cookies to store. + + func storeCookies(_ cookies: [HTTPCookie]) async throws { + let cookieData = try HTTPCookieCodec.encodeCookies(cookies) + try await storeCookieData(cookieData) + } + + /// Fetch stored cookies. + /// + /// Note: Cookie data may be persisted across installations for the same + /// account, however it is likely that fetching an old cookie would result + /// in a decoding error. + /// + /// - Returns: The stored cookies. + + func fetchCookies() async throws -> [HTTPCookie] { + guard let cookieData = try await fetchCookieData() else { + return [] + } + + return try HTTPCookieCodec.decodeData(cookieData) + } + + // MARK: - Cookie data + + private func storeCookieData(_ cookieData: Data) async throws { + let encryptedCookieData: Data + do { + encryptedCookieData = try AES256Crypto.encryptAllAtOnceWithPrefixedIV( + plaintext: cookieData, + key: cookieEncryptionKey + ).data + } catch { + throw Failure.failedToEncryptCookie(error) + } + + if try await fetchCookieData() != nil { + try await updateCookieInKeychain(encryptedCookieData) + } else { + try await addCookieToKeychain(encryptedCookieData) + } + } + + private func fetchCookieData() async throws -> Data? { + guard let encryptedCookieData = try await fetchCookieDataFromKeychain() else { + return nil + } + + do { + return try AES256Crypto.decryptAllAtOnceWithPrefixedIV( + ciphertext: AES256Crypto.PrefixedData(data: encryptedCookieData), + key: cookieEncryptionKey + ) + } catch { + throw Failure.failedToDecryptCookie(error) + } + } + + // MARK: - Keychain + + private func addCookieToKeychain(_ cookieData: Data) async throws { + let query = addQuery(cookieData: cookieData) + try await keychain.addItem(query: query) + } + + private func updateCookieInKeychain(_ cookieData: Data) async throws { + let updateQuery = updateQuery(cookieData: cookieData) + try await keychain.updateItem(query: fetchQuery, attributesToUpdate: updateQuery) + } + + private func fetchCookieDataFromKeychain() async throws -> Data? { + guard let base64CookieData: Data = try await keychain.fetchItem(query: fetchQuery) else { + return nil + } + + guard let cookieData = Data(base64Encoded: base64CookieData) else { + throw Failure.malformedCookieData + } + + return cookieData + } + +} diff --git a/WireAPI/Sources/WireAPI/Authentication/HTTPCodec.swift b/WireAPI/Sources/WireAPI/Authentication/HTTPCodec.swift new file mode 100644 index 00000000000..e524615db01 --- /dev/null +++ b/WireAPI/Sources/WireAPI/Authentication/HTTPCodec.swift @@ -0,0 +1,60 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +enum HTTPCookieCodec { + + static func encodeCookies(_ cookies: [HTTPCookie]) throws -> Data { + let properties = cookies.compactMap(\.properties) + + guard + let name = properties.first?[.name] as? String, + name == "zuid" + else { + throw HTTPCookieCodecError.invalidCookies + } + + let archiver = NSKeyedArchiver(requiringSecureCoding: true) + archiver.encode(properties, forKey: "properties") + archiver.finishEncoding() + + return archiver.encodedData + } + + static func decodeData(_ data: Data) throws -> [HTTPCookie] { + let unarchiver: NSKeyedUnarchiver + do { + unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) + unarchiver.requiresSecureCoding = true + } catch { + throw HTTPCookieCodecError.invalidCookieData(reason: String(describing: error)) + } + + guard let propertyList = unarchiver.decodePropertyList(forKey: "properties") else { + throw HTTPCookieCodecError.invalidCookieData(reason: "no value for 'properties' key") + } + + guard let properties = propertyList as? [[HTTPCookiePropertyKey: Any]] else { + throw HTTPCookieCodecError.invalidCookieData(reason: "'properties' has invalid type") + } + + return properties.compactMap(HTTPCookie.init) + } + +} diff --git a/WireFoundation/Tests/WireUtilitiesTests/PlaceholderTests.swift b/WireAPI/Sources/WireAPI/Authentication/HTTPCodecError.swift similarity index 85% rename from WireFoundation/Tests/WireUtilitiesTests/PlaceholderTests.swift rename to WireAPI/Sources/WireAPI/Authentication/HTTPCodecError.swift index a46802a0f75..f0cc916306c 100644 --- a/WireFoundation/Tests/WireUtilitiesTests/PlaceholderTests.swift +++ b/WireAPI/Sources/WireAPI/Authentication/HTTPCodecError.swift @@ -16,11 +16,11 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest +import Foundation -@testable import WireUtilitiesPackage +enum HTTPCookieCodecError: Error { -final class PlaceholderTests: XCTestCase { + case invalidCookies + case invalidCookieData(reason: String) - func testNothing() {} } diff --git a/WireAPI/Sources/WireAPI/Components/AuthenticationStorage/InMemoryAuthenticationStorage.swift b/WireAPI/Sources/WireAPI/Components/AuthenticationStorage/InMemoryAuthenticationStorage.swift deleted file mode 100644 index a0570897b22..00000000000 --- a/WireAPI/Sources/WireAPI/Components/AuthenticationStorage/InMemoryAuthenticationStorage.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Wire -// Copyright (C) 2024 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import Foundation - -final class InMemoryAuthenticationStorage: AuthenticationStorage { - - private var accessToken: AccessToken? - private var cookieData: Data? - - func storeAccessToken(_ accessToken: AccessToken) { - self.accessToken = accessToken - } - - func fetchAccessToken() -> AccessToken? { - accessToken - } - - func storeCookieData(_ cookieData: Data?) { - self.cookieData = cookieData - } - - func fetchCookieData() -> Data? { - cookieData - } - -} diff --git a/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift b/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift index 3f6a3547dc6..8f33ef98474 100644 --- a/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift +++ b/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift @@ -34,6 +34,10 @@ enum HTTPStatusCode: Int { case badRequest = 400 + /// unauthorized - 401 + + case unauthorized = 401 + /// not found - 404 case notFound = 404 diff --git a/WireAPI/Sources/WireAPI/Models/Authorization/AccessToken.swift b/WireAPI/Sources/WireAPI/Models/Authorization/AccessToken.swift index 67fbe0c8741..fa2494d8d4b 100644 --- a/WireAPI/Sources/WireAPI/Models/Authorization/AccessToken.swift +++ b/WireAPI/Sources/WireAPI/Models/Authorization/AccessToken.swift @@ -21,7 +21,7 @@ import Foundation /// A token used to make authenticated requests to /// the backend. -public struct AccessToken { +public struct AccessToken: Equatable, Sendable { /// The user id of whom the token belongs. @@ -35,8 +35,8 @@ public struct AccessToken { public let type: String - /// The number of seconds the token is valid. + /// The point in time the token expires. - public let validityInSeconds: TimeInterval + public let expirationDate: Date } diff --git a/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift b/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift index 72e6afe660f..c59ef047635 100644 --- a/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift +++ b/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift @@ -17,6 +17,7 @@ // import Foundation +import WireFoundation // sourcery: AutoMockable /// A service for network communication to a specific backend. @@ -50,19 +51,23 @@ public protocol APIServiceProtocol { public final class APIService: APIServiceProtocol { private let networkService: NetworkService - private let authenticationStorage: any AuthenticationStorage + private let authenticationManager: any AuthenticationManagerProtocol /// Create a new `APIService`. /// /// - Parameters: + /// - clientID: The id of the self client. /// - backendURL: The url of the target backend. /// - authenticationStorage: The storage for authentication objects. /// - minTLSVersion: The minimum supported TLS version. public convenience init( + userID: UUID, + clientID: String, backendURL: URL, - authenticationStorage: any AuthenticationStorage, - minTLSVersion: TLSVersion + minTLSVersion: TLSVersion, + cookieEncryptionKey: Data, + keychain: any KeychainProtocol ) { let configFactory = URLSessionConfigurationFactory(minTLSVersion: minTLSVersion) let configuration = configFactory.makeRESTAPISessionConfiguration() @@ -70,18 +75,30 @@ public final class APIService: APIServiceProtocol { let urlSession = URLSession(configuration: configuration) networkService.configure(with: urlSession) + let cookieStorage = CookieStorage( + userID: userID, + cookieEncryptionKey: cookieEncryptionKey, + keychain: keychain + ) + + let authenticationManager = AuthenticationManager( + clientID: clientID, + cookieStorage: cookieStorage, + networkService: networkService + ) + self.init( networkService: networkService, - authenticationStorage: authenticationStorage + authenticationManager: authenticationManager ) } init( networkService: NetworkService, - authenticationStorage: any AuthenticationStorage + authenticationManager: any AuthenticationManagerProtocol ) { self.networkService = networkService - self.authenticationStorage = authenticationStorage + self.authenticationManager = authenticationManager } /// Execute a request to the backend. @@ -99,14 +116,23 @@ public final class APIService: APIServiceProtocol { var request = request if requiringAccessToken { - guard let accessToken = authenticationStorage.fetchAccessToken() else { - throw APIServiceError.missingAccessToken - } - + let accessToken = try await authenticationManager.getValidAccessToken() request.setAccessToken(accessToken) } - return try await networkService.executeRequest(request) + let firstAttempt = try await networkService.executeRequest(request) + + // If we get an authentication error, it could be that we erroneously + // thought we had a valid access token (e.g the device moved to a new + // timezone and we miscalculated its expiry date). We'll attempt a + // single retry with a new token just in case. + if HTTPStatusCode(rawValue: firstAttempt.1.statusCode) == .unauthorized { + let accessToken = try await authenticationManager.refreshAccessToken() + request.setAccessToken(accessToken) + return try await networkService.executeRequest(request) + } else { + return firstAttempt + } } } diff --git a/WireAPI/Sources/WireAPI/Network/APIService/APIServiceError.swift b/WireAPI/Sources/WireAPI/Network/APIService/APIServiceError.swift deleted file mode 100644 index 3ede9209ef8..00000000000 --- a/WireAPI/Sources/WireAPI/Network/APIService/APIServiceError.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Wire -// Copyright (C) 2024 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import Foundation - -/// Errors originating from `APIService`. - -public enum APIServiceError: Error { - - /// An access token is required but none is available. - - case missingAccessToken - -} diff --git a/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift b/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift index 482ac53efd0..eb587850af8 100644 --- a/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift +++ b/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift @@ -26,7 +26,7 @@ public protocol PushChannelServiceProtocol { /// - Parameter request: A request for a web socket connection. /// - Returns: A push channel. - func createPushChannel(_ request: URLRequest) throws -> any PushChannelProtocol + func createPushChannel(_ request: URLRequest) async throws -> any PushChannelProtocol } @@ -66,10 +66,10 @@ public final class PushChannelService: PushChannelServiceProtocol { self.authenticationStorage = authenticationStorage } - public func createPushChannel(_ request: URLRequest) throws -> any PushChannelProtocol { + public func createPushChannel(_ request: URLRequest) async throws -> any PushChannelProtocol { var request = request - guard let accessToken = authenticationStorage.fetchAccessToken() else { + guard let accessToken = await authenticationStorage.fetchAccessToken() else { throw PushChannelServiceError.missingAccessToken } diff --git a/WireAPI/Sources/WireAPI/Network/URLRequestBuilder/URLRequestBuilder.swift b/WireAPI/Sources/WireAPI/Network/URLRequestBuilder/URLRequestBuilder.swift index 6cbc256a186..1a3b4673146 100644 --- a/WireAPI/Sources/WireAPI/Network/URLRequestBuilder/URLRequestBuilder.swift +++ b/WireAPI/Sources/WireAPI/Network/URLRequestBuilder/URLRequestBuilder.swift @@ -38,6 +38,20 @@ struct URLRequestBuilder { request } + func withQueryItem( + name: String, + value: String + ) -> Self { + withCopy { + let queryItem = URLQueryItem( + name: name, + value: value + ) + + $0.request.url?.append(queryItems: [queryItem]) + } + } + func withMethod(_ method: HTTPMethod) -> Self { withCopy { $0.request.httpMethod = method.rawValue @@ -66,6 +80,35 @@ struct URLRequestBuilder { } } + func addingHeader( + field: String, + value: String + ) -> Self { + addingHeaders([field: value]) + } + + func addingHeaders(_ headers: [String: String]) -> Self { + withCopy { + for (field, value) in headers { + $0.request.addValue( + value, + forHTTPHeaderField: field + ) + } + } + } + + func withCookies(_ cookies: [HTTPCookie]) -> Self { + withCopy { + for (field, value) in HTTPCookie.requestHeaderFields(with: cookies) { + $0.request.addValue( + value, + forHTTPHeaderField: field + ) + } + } + } + private func withCopy(_ mutation: (inout Self) -> Void) -> Self { var copy = self mutation(©) diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift index 17e93b7f350..9b3b7f67d4f 100644 --- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift @@ -539,7 +539,7 @@ final class ConversationsAPITests: XCTestCase { for sut in suts { taskGroup.addTask { // Then - await self.XCTAssertThrowsError(ConversationsAPIError.unsupportedEndpointForAPIVersion) { + await self.XCTAssertThrowsErrorAsync(ConversationsAPIError.unsupportedEndpointForAPIVersion) { // When try await sut.getMLSOneToOneConversation( userID: Scaffolding.userID, @@ -565,7 +565,7 @@ final class ConversationsAPITests: XCTestCase { // Then - await XCTAssertThrowsError(ConversationsAPIError.mlsNotEnabled) { + await XCTAssertThrowsErrorAsync(ConversationsAPIError.mlsNotEnabled) { // When try await sut.getMLSOneToOneConversation( userID: Scaffolding.userID, @@ -586,7 +586,7 @@ final class ConversationsAPITests: XCTestCase { // Then - await XCTAssertThrowsError(ConversationsAPIError.usersNotConnected) { + await XCTAssertThrowsErrorAsync(ConversationsAPIError.usersNotConnected) { // When try await sut.getMLSOneToOneConversation( userID: Scaffolding.userID, @@ -607,7 +607,7 @@ final class ConversationsAPITests: XCTestCase { // Then - await XCTAssertThrowsError(ConversationsAPIError.userAndDomainShouldNotBeEmpty) { + await XCTAssertThrowsErrorAsync(ConversationsAPIError.userAndDomainShouldNotBeEmpty) { // When try await sut.getMLSOneToOneConversation( userID: "", diff --git a/WireAPI/Tests/WireAPITests/APIs/FeatureConfigsAPI/FeatureConfigsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/FeatureConfigsAPI/FeatureConfigsAPITests.swift index a8eb5def59b..5d861472aa7 100644 --- a/WireAPI/Tests/WireAPITests/APIs/FeatureConfigsAPI/FeatureConfigsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/FeatureConfigsAPI/FeatureConfigsAPITests.swift @@ -107,7 +107,7 @@ final class FeatureConfigsAPITests: XCTestCase { let sut = FeatureConfigsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(FeatureConfigsAPIError.teamNotFound) { + await XCTAssertThrowsErrorAsync(FeatureConfigsAPIError.teamNotFound) { // When try await sut.getFeatureConfigs() } @@ -119,7 +119,7 @@ final class FeatureConfigsAPITests: XCTestCase { let sut = FeatureConfigsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(FeatureConfigsAPIError.userIsNotTeamMember) { + await XCTAssertThrowsErrorAsync(FeatureConfigsAPIError.userIsNotTeamMember) { // When try await sut.getFeatureConfigs() } @@ -131,7 +131,7 @@ final class FeatureConfigsAPITests: XCTestCase { let sut = FeatureConfigsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(FeatureConfigsAPIError.insufficientPermissions) { + await XCTAssertThrowsErrorAsync(FeatureConfigsAPIError.insufficientPermissions) { // When try await sut.getFeatureConfigs() } diff --git a/WireAPI/Tests/WireAPITests/APIs/SelfUserAPI/SelfUserAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/SelfUserAPI/SelfUserAPITests.swift index a3c0c2b3745..60ce2febefc 100644 --- a/WireAPI/Tests/WireAPITests/APIs/SelfUserAPI/SelfUserAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/SelfUserAPI/SelfUserAPITests.swift @@ -70,7 +70,7 @@ final class SelfUserAPITests: XCTestCase { for sut in suts { taskGroup.addTask { // Then - await self.XCTAssertThrowsError(SelfUserAPIError.unsupportedEndpointForAPIVersion) { + await self.XCTAssertThrowsErrorAsync(SelfUserAPIError.unsupportedEndpointForAPIVersion) { // When try await sut.pushSupportedProtocols([.mls]) } @@ -135,7 +135,7 @@ final class SelfUserAPITests: XCTestCase { let sut = SelfUserAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError { + await XCTAssertThrowsErrorAsync { // When try await sut.getSelfUser() } @@ -181,7 +181,7 @@ final class SelfUserAPITests: XCTestCase { let sut = SelfUserAPIV5(httpClient: httpClient) // Then - await XCTAssertThrowsError { + await XCTAssertThrowsErrorAsync { // When try await sut.pushSupportedProtocols([.mls]) } diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift index 30bff2a0541..41552dab264 100644 --- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift @@ -102,7 +102,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidTeamID) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidTeamID) { // When try await sut.getTeam(for: Team.ID()) } @@ -114,7 +114,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.teamNotFound) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.teamNotFound) { // When try await sut.getTeam(for: Team.ID()) } @@ -153,7 +153,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.selfUserIsNotTeamMember) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.selfUserIsNotTeamMember) { // When try await sut.getTeamRoles(for: Team.ID()) } @@ -165,7 +165,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.teamNotFound) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.teamNotFound) { // When try await sut.getTeamRoles(for: Team.ID()) } @@ -213,7 +213,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidQueryParmeter) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidQueryParmeter) { // When try await sut.getTeamMembers( for: Team.ID(), @@ -228,7 +228,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.selfUserIsNotTeamMember) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.selfUserIsNotTeamMember) { // When try await sut.getTeamMembers( for: Team.ID(), @@ -243,7 +243,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.teamNotFound) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.teamNotFound) { // When try await sut.getTeamMembers( for: Team.ID(), @@ -281,7 +281,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidRequest) { // When try await sut.getLegalholdStatus( for: Team.ID(), @@ -296,7 +296,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.teamMemberNotFound) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.teamMemberNotFound) { // When try await sut.getLegalholdStatus( for: Team.ID(), @@ -342,7 +342,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidTeamID) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidTeamID) { // When try await sut.getTeam(for: Team.ID()) } @@ -354,7 +354,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.teamNotFound) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.teamNotFound) { // When try await sut.getTeamRoles(for: Team.ID()) } @@ -366,7 +366,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidRequest) { // When try await sut.getTeamMembers( for: Team.ID(), @@ -381,7 +381,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidRequest) { // When try await sut.getLegalholdStatus( for: Team.ID(), @@ -398,7 +398,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV5(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidTeamID) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidTeamID) { // When try await sut.getTeam(for: Team.ID()) } @@ -410,7 +410,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV5(httpClient: httpClient) // Then - await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { + await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidRequest) { // When try await sut.getLegalholdStatus( for: Team.ID(), diff --git a/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift index 81ee01c5d1e..06883db00ce 100644 --- a/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift @@ -80,7 +80,7 @@ final class UpdateEventsAPITests: XCTestCase { let sut = UpdateEventsAPIV0(apiService: apiService) // Then - await XCTAssertThrowsError(UpdateEventsAPIError.invalidClient) { + await XCTAssertThrowsErrorAsync(UpdateEventsAPIError.invalidClient) { // When try await sut.getLastUpdateEvent(selfClientID: Scaffolding.selfClientID) } @@ -96,7 +96,7 @@ final class UpdateEventsAPITests: XCTestCase { let sut = UpdateEventsAPIV0(apiService: apiService) // Then - await XCTAssertThrowsError(UpdateEventsAPIError.notFound) { + await XCTAssertThrowsErrorAsync(UpdateEventsAPIError.notFound) { // When try await sut.getLastUpdateEvent(selfClientID: Scaffolding.selfClientID) } @@ -136,7 +136,7 @@ final class UpdateEventsAPITests: XCTestCase { let sut = UpdateEventsAPIV0(apiService: apiService) // Then - await XCTAssertThrowsError(UpdateEventsAPIError.invalidParameters) { + await XCTAssertThrowsErrorAsync(UpdateEventsAPIError.invalidParameters) { // When for try await _ in sut.getUpdateEvents( selfClientID: Scaffolding.selfClientID, @@ -153,7 +153,7 @@ final class UpdateEventsAPITests: XCTestCase { let sut = UpdateEventsAPIV0(apiService: apiService) // Then - await XCTAssertThrowsError(UpdateEventsAPIError.notFound) { + await XCTAssertThrowsErrorAsync(UpdateEventsAPIError.notFound) { // When for try await _ in sut.getUpdateEvents( selfClientID: Scaffolding.selfClientID, @@ -191,7 +191,7 @@ final class UpdateEventsAPITests: XCTestCase { let sut = UpdateEventsAPIV5(apiService: apiService) // Then - await XCTAssertThrowsError(UpdateEventsAPIError.notFound) { + await XCTAssertThrowsErrorAsync(UpdateEventsAPIError.notFound) { // When try await sut.getLastUpdateEvent(selfClientID: Scaffolding.selfClientID) } @@ -231,7 +231,7 @@ final class UpdateEventsAPITests: XCTestCase { let sut = UpdateEventsAPIV5(apiService: apiService) // Then - await XCTAssertThrowsError(UpdateEventsAPIError.notFound) { + await XCTAssertThrowsErrorAsync(UpdateEventsAPIError.notFound) { // When for try await _ in sut.getUpdateEvents( selfClientID: Scaffolding.selfClientID, diff --git a/WireAPI/Tests/WireAPITests/APIs/UserPropertiesAPI/UserPropertiesAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/UserPropertiesAPI/UserPropertiesAPITests.swift index bd2cb7ed4a6..e8f39d819c6 100644 --- a/WireAPI/Tests/WireAPITests/APIs/UserPropertiesAPI/UserPropertiesAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/UserPropertiesAPI/UserPropertiesAPITests.swift @@ -125,7 +125,7 @@ final class UserPropertiesAPITests: XCTestCase { let sut = UserPropertiesAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(UserPropertiesAPIError.propertyNotFound) { + await XCTAssertThrowsErrorAsync(UserPropertiesAPIError.propertyNotFound) { // When _ = try await sut.getLabels() } @@ -137,7 +137,7 @@ final class UserPropertiesAPITests: XCTestCase { let sut = UserPropertiesAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(UserPropertiesAPIError.propertyNotFound) { + await XCTAssertThrowsErrorAsync(UserPropertiesAPIError.propertyNotFound) { // When _ = try await sut.areTypingIndicatorsEnabled } @@ -149,7 +149,7 @@ final class UserPropertiesAPITests: XCTestCase { let sut = UserPropertiesAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(UserPropertiesAPIError.propertyNotFound) { + await XCTAssertThrowsErrorAsync(UserPropertiesAPIError.propertyNotFound) { // When _ = try await sut.areReadReceiptsEnabled } @@ -163,7 +163,7 @@ final class UserPropertiesAPITests: XCTestCase { let sut = UserPropertiesAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(UserPropertiesAPIError.invalidKey) { + await XCTAssertThrowsErrorAsync(UserPropertiesAPIError.invalidKey) { // When try await sut.getLabels() } diff --git a/WireAPI/Tests/WireAPITests/APIs/UsersAPI/UsersAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/UsersAPI/UsersAPITests.swift index 5a828dec01e..e42ed4576e5 100644 --- a/WireAPI/Tests/WireAPITests/APIs/UsersAPI/UsersAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/UsersAPI/UsersAPITests.swift @@ -100,7 +100,7 @@ final class UsersAPITests: XCTestCase { let sut = UsersAPIV0(httpClient: httpClient) // Then - await XCTAssertThrowsError(UsersAPIError.userNotFound) { + await XCTAssertThrowsErrorAsync(UsersAPIError.userNotFound) { // When try await sut.getUser(for: Scaffolding.userID) } @@ -135,7 +135,7 @@ final class UsersAPITests: XCTestCase { let sut = UsersAPIV4(httpClient: httpClient) // Then - await XCTAssertThrowsError(UsersAPIError.userNotFound) { + await XCTAssertThrowsErrorAsync(UsersAPIError.userNotFound) { // When try await sut.getUser(for: Scaffolding.userID) } diff --git a/WireAPI/Tests/WireAPITests/Authentication/AuthenticationManagerTests.swift b/WireAPI/Tests/WireAPITests/Authentication/AuthenticationManagerTests.swift new file mode 100644 index 00000000000..84b244b7bcf --- /dev/null +++ b/WireAPI/Tests/WireAPITests/Authentication/AuthenticationManagerTests.swift @@ -0,0 +1,225 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import XCTest + +@testable import WireAPI +@testable import WireAPISupport + +final class AuthenticationManagerTests: XCTestCase { + + var sut: AuthenticationManager! + var backendURL: URL! + var cookieStorage: MockCookieStorageProtocol! + + override func setUpWithError() throws { + cookieStorage = MockCookieStorageProtocol() + backendURL = try XCTUnwrap(URL(string: "https://www.example.com")) + let networkService = NetworkService(baseURL: backendURL) + networkService.configure(with: .mockURLSession()) + + sut = AuthenticationManager( + clientID: Scaffolding.clientID, + cookieStorage: cookieStorage, + networkService: networkService + ) + } + + override func tearDown() { + cookieStorage = nil + backendURL = nil + sut = nil + } + + // MARK: - Get a valid token + + func testGetValidAccessToken_CacheIsEmpty() async throws { + // Mock valid cookie. + cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] + + // Mock successful token response. + var receivedRequests = [URLRequest]() + URLProtocolMock.mockHandler = { + receivedRequests.append($0) + return try $0.mockResponse( + statusCode: .ok, + jsonResourceName: "PostAccessSuccessResponse200" + ) + } + + // When we get a valid token the first time + let accessToken = try await sut.getValidAccessToken() + + // Then a request was made to get a new access token. + try XCTAssertCount(receivedRequests, count: 1) + let snapshotter = HTTPRequestSnapshotHelper() + await snapshotter.verifyRequest(request: receivedRequests[0]) + + // Then we got back a valid access token. + XCTAssertEqual(accessToken.userID, Scaffolding.userID) + XCTAssertEqual(accessToken.type, Scaffolding.tokenType) + XCTAssertEqual(accessToken.token, Scaffolding.validAccessToken) + XCTAssertFalse(accessToken.isExpiring) + + // When we ask for the token again. + receivedRequests.removeAll() + let secondAccessToken = try await sut.getValidAccessToken() + + // Then no new requests were made. + XCTAssertTrue(receivedRequests.isEmpty) + + // Then it's the same (cached) token. + XCTAssertEqual(secondAccessToken, accessToken) + } + + func testGetValidAccessToken_CacheHitButItIsExpiring() async throws { + // Given an existing but expiring. + let cachedToken = try await setCachedExpiringAccessToken() + XCTAssertTrue(cachedToken.isExpiring) + + // Mock successful token response. + var receivedRequests = [URLRequest]() + URLProtocolMock.mockHandler = { + receivedRequests.append($0) + return try $0.mockResponse( + statusCode: .ok, + jsonResourceName: "PostAccessSuccessResponse200" + ) + } + + // When we ask for a valid token. + let accessToken = try await sut.getValidAccessToken() + + // Then a request was made to get a new access token. + try XCTAssertCount(receivedRequests, count: 1) + let snapshotter = HTTPRequestSnapshotHelper() + await snapshotter.verifyRequest(request: receivedRequests[0]) + + // Then we got back a vaild access token. + XCTAssertEqual(accessToken.userID, Scaffolding.userID) + XCTAssertEqual(accessToken.type, Scaffolding.tokenType) + XCTAssertEqual(accessToken.token, Scaffolding.validAccessToken) + XCTAssertFalse(accessToken.isExpiring) + } + + private func setCachedExpiringAccessToken() async throws -> AccessToken { + cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] + + URLProtocolMock.mockHandler = { + try $0.mockResponse( + statusCode: .ok, + jsonResourceName: "ExpiringAccessTokenResponse" + ) + } + + return try await sut.getValidAccessToken() + } + + func testGetValidAccessToken_AwaitTokenRefresh() async throws { + // Mock valid cookie. + cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] + + // Mock successful token response. + var receivedRequests = [URLRequest]() + URLProtocolMock.mockHandler = { + receivedRequests.append($0) + return try $0.mockResponse( + statusCode: .ok, + jsonResourceName: "PostAccessSuccessResponse200" + ) + } + + // When multiple tasks all want an access token. + await withThrowingTaskGroup(of: Void.self) { group in + for _ in 1 ... 10 { + group.addTask { [sut] in + guard let sut else { return } + let accessToken = try await sut.getValidAccessToken() + + // Then each task go back a valid access token. + XCTAssertEqual(accessToken.userID, Scaffolding.userID) + XCTAssertEqual(accessToken.type, Scaffolding.tokenType) + XCTAssertEqual(accessToken.token, Scaffolding.validAccessToken) + XCTAssertFalse(accessToken.isExpiring) + } + } + } + + // Then only one request was made to get a new access token. + try XCTAssertCount(receivedRequests, count: 1) + let snapshotter = HTTPRequestSnapshotHelper() + await snapshotter.verifyRequest(request: receivedRequests[0]) + } + + // MARK: - Refresh access token + + func testRefreshAccessToken_AfterAnError_WeCanStillRefresh() async throws { + // Mock token refresh error. + cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] + URLProtocolMock.mockHandler = { + try $0.mockErrorResponse( + statusCode: .forbidden, + label: "invalid-credentials" + ) + } + + // Then + await XCTAssertThrowsErrorAsync(AuthenticationManager.Failure.invalidCredentials) { + // When a new token is requested. + try await self.sut.refreshAccessToken() + } + + // Mock a successful token refresh. + URLProtocolMock.mockHandler = { + try $0.mockResponse( + statusCode: .ok, + jsonResourceName: "PostAccessSuccessResponse200" + ) + } + + // When we try again. + let accessToken = try await sut.refreshAccessToken() + + // Then it succeeds. + XCTAssertEqual(accessToken.userID, Scaffolding.userID) + XCTAssertEqual(accessToken.type, Scaffolding.tokenType) + XCTAssertEqual(accessToken.token, Scaffolding.validAccessToken) + XCTAssertFalse(accessToken.isExpiring) + } + +} + +private enum Scaffolding { + + static let userID = UUID(uuidString: "70aa272d-3413-4cda-9059-64c097956583")! + static let clientID = "abc123" + static let tokenType = "Bearer" + static let validAccessToken = "a-valid-access-token" + + static func cookie() throws -> HTTPCookie { + try XCTUnwrap( + HTTPCookie(properties: [ + .name: "zuid", + .path: "some path", + .value: "some value", + .domain: "some domain" + ]) + ) + } + +} diff --git a/WireAPI/Tests/WireAPITests/Authentication/CookieStorageTests.swift b/WireAPI/Tests/WireAPITests/Authentication/CookieStorageTests.swift new file mode 100644 index 00000000000..a5bb46c263e --- /dev/null +++ b/WireAPI/Tests/WireAPITests/Authentication/CookieStorageTests.swift @@ -0,0 +1,295 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireFoundation +import WireTestingPackage +import XCTest + +@testable import WireAPI +@testable import WireFoundationSupport + +final class CookieStorageTests: XCTestCase { + + var sut: CookieStorage! + var cookieEncryptionKey: Data! + var keychain: MockKeychainProtocol! + + override func setUpWithError() throws { + cookieEncryptionKey = try Scaffolding.cookieEncryptionKey() + keychain = MockKeychainProtocol() + sut = CookieStorage( + userID: Scaffolding.userID, + cookieEncryptionKey: cookieEncryptionKey, + keychain: keychain + ) + } + + override func tearDown() { + cookieEncryptionKey = nil + keychain = nil + sut = nil + } + + // MARK: - Cookies + + func testStoreCookies_No_Cookies() async throws { + // Given + let cookies = [HTTPCookie]() + + // Then + await XCTAssertThrowsErrorAsync({ + // When + try await sut.storeCookies(cookies) + }, errorHandler: { error in + guard case HTTPCookieCodecError.invalidCookies = error else { + XCTFail("unexpected error: \(error)") + return + } + }) + } + + func testStoreCookies_Invalid_Cookie() async throws { + // Given + let invalidCookie = try XCTUnwrap(Scaffolding.invalidCookie) + + // Then + await XCTAssertThrowsErrorAsync({ + // When + try await sut.storeCookies([invalidCookie]) + }, errorHandler: { error in + guard case HTTPCookieCodecError.invalidCookies = error else { + XCTFail("unexpected error: \(error)") + return + } + }) + } + + func testStoreCookies_Adds_To_Keychain() async throws { + // Given + let validCookie = try XCTUnwrap(Scaffolding.validCookie) + + // Mock no existing cookie. + await keychain.setFetchItemQuery_MockValue(nil) + + // Mock successul add. + await keychain.setAddItemQuery_MockMethod { _ in } + + // When + try await sut.storeCookies([validCookie]) + + // Then first we tried to fetch an existing cookie. + let fetchInvocations = await keychain.fetchItemQuery_Invocations + try XCTAssertCount(fetchInvocations, count: 1) + XCTAssertEqual(fetchInvocations[0], Scaffolding.fetchQuery) + + // Then we added the new cookie. + let addInvocations = await keychain.addItemQuery_Invocations + try XCTAssertCount(addInvocations, count: 1) + try assertAddQuery(addInvocations[0], addedCookie: validCookie) + } + + func testStoreCookies_Updates_Keychain() async throws { + // Given + let validCookie = try XCTUnwrap(Scaffolding.validCookie) + + // Mock existing cookie. + let data = Data("raw cookie".utf8).base64EncodedData() + await keychain.setFetchItemQuery_MockValue(data) + + // Mock successul update. + await keychain.setUpdateItemQueryAttributesToUpdate_MockMethod { _, _ in } + + // When + try await sut.storeCookies([validCookie]) + + // Then first we tried to fetch an existing cookie. + let fetchInvocations = await keychain.fetchItemQuery_Invocations + try XCTAssertCount(fetchInvocations, count: 1) + XCTAssertEqual(fetchInvocations[0], Scaffolding.fetchQuery) + + // Then we updated the keychain with the new cookie. + let updateInvocations = await keychain.updateItemQueryAttributesToUpdate_Invocations + try XCTAssertCount(updateInvocations, count: 1) + + XCTAssertEqual(updateInvocations[0].query, Scaffolding.fetchQuery) + try assertUpdateQuery(updateInvocations[0].attributesToUpdate, updatedCookie: validCookie) + } + + func testFetchCookies_No_Cookies_EXist() async throws { + // Mock no existing cookie. + await keychain.setFetchItemQuery_MockValue(nil) + + // When + let cookies = try await sut.fetchCookies() + + // Then + XCTAssertTrue(cookies.isEmpty) + } + + func testFetchCookies() async throws { + // Given + let validCookie = try XCTUnwrap(Scaffolding.validCookie) + let storedCookieData = try Scaffolding.encodeAndEncryptCookieData( + for: [validCookie], + encryptionKey: cookieEncryptionKey + ) + + // Mock existing cookie. + await keychain.setFetchItemQuery_MockValue(storedCookieData) + + // When + let cookies = try await sut.fetchCookies() + + // Then + try assertCookies( + cookies, + equals: validCookie + ) + } + + // MARK: - Helpers + + private func assertAddQuery( + _ query: Set, + addedCookie: HTTPCookie, + file: StaticString = #file, + line: UInt = #line + ) throws { + XCTAssertTrue(query.contains(.accessible(.afterFirstUnlock)), file: file, line: line) + try assertUpdateQuery(query, updatedCookie: addedCookie, file: file, line: line) + } + + private func assertUpdateQuery( + _ query: Set, + updatedCookie: HTTPCookie, + file: StaticString = #file, + line: UInt = #line + ) throws { + XCTAssertTrue(query.contains(.service("Wire: Credentials for wire.com")), file: file, line: line) + XCTAssertTrue(query.contains(.account(Scaffolding.userID.uuidString)), file: file, line: line) + XCTAssertTrue(query.contains(.itemClass(.genericPassword)), file: file, line: line) + + var storedData: Data? + for item in query { + if case let .data(data) = item { + storedData = data + break + } + } + + let encryptedCookieData = try XCTUnwrap(storedData, file: file, line: line) + assertStoredCookieData(encryptedCookieData, equals: updatedCookie, file: file, line: line) + } + + private func assertStoredCookieData( + _ storedCookieData: Data, + equals cookie: HTTPCookie, + file: StaticString = #file, + line: UInt = #line + ) { + do { + let actualHTTPCookies = try Scaffolding.decryptAndDecodeCookieData( + storedCookieData, + encryptionKey: cookieEncryptionKey + ) + try assertCookies( + actualHTTPCookies, + equals: cookie, + file: file, + line: line + ) + } catch { + XCTFail( + "failed to assert cookie data: \(error)", + file: file, + line: line + ) + } + } + + private func assertCookies( + _ cookies: [HTTPCookie], + equals cookie: HTTPCookie, + file: StaticString = #file, + line: UInt = #line + ) throws { + try XCTAssertCount(cookies, count: 1, file: file, line: line) + XCTAssertEqual(cookies[0].name, cookie.name, file: file, line: line) + XCTAssertEqual(cookies[0].value, cookie.value, file: file, line: line) + XCTAssertEqual(cookies[0].path, cookie.path, file: file, line: line) + XCTAssertEqual(cookies[0].domain, cookie.domain, file: file, line: line) + } + +} + +private enum Scaffolding { + + static let userID = UUID() + + static func cookieEncryptionKey() throws -> Data { + try AES256Crypto.generateRandomEncryptionKey() + } + + static var fetchQuery: Set { + [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword), + .returningData(true) + ] + } + + static let invalidCookie = HTTPCookie(properties: [ + .name: "invalid-name", + .path: "some path", + .value: "some value", + .domain: "some domain" + ]) + + static let validCookie = HTTPCookie(properties: [ + .name: "zuid", + .path: "some path", + .value: "some value", + .domain: "some domain" + ]) + + static func encodeAndEncryptCookieData( + for cookies: [HTTPCookie], + encryptionKey: Data + ) throws -> Data { + let encodedData = try HTTPCookieCodec.encodeCookies(cookies) + let encryptedData = try AES256Crypto.encryptAllAtOnceWithPrefixedIV( + plaintext: encodedData, + key: encryptionKey + ) + return encryptedData.data.base64EncodedData() + } + + static func decryptAndDecodeCookieData( + _ base64CookieData: Data, + encryptionKey: Data + ) throws -> [HTTPCookie] { + let encryptedData = try XCTUnwrap(Data(base64Encoded: base64CookieData)) + let decryptedData = try AES256Crypto.decryptAllAtOnceWithPrefixedIV( + ciphertext: AES256Crypto.PrefixedData(data: encryptedData), + key: encryptionKey + ) + return try HTTPCookieCodec.decodeData(decryptedData) + } + +} diff --git a/WireAPI/Tests/WireAPITests/Authentication/Resources/ExpiringAccessTokenResponse.json b/WireAPI/Tests/WireAPITests/Authentication/Resources/ExpiringAccessTokenResponse.json new file mode 100644 index 00000000000..7b6265820b1 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/Authentication/Resources/ExpiringAccessTokenResponse.json @@ -0,0 +1,6 @@ +{ + "user": "70aa272d-3413-4cda-9059-64c097956583", + "access_token": "an-expiring-access-token", + "token_type": "Bearer", + "expires_in": 10 +} diff --git a/WireAPI/Tests/WireAPITests/Authentication/Resources/PostAccessSuccessResponse200.json b/WireAPI/Tests/WireAPITests/Authentication/Resources/PostAccessSuccessResponse200.json new file mode 100644 index 00000000000..bf8c8ad51c8 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/Authentication/Resources/PostAccessSuccessResponse200.json @@ -0,0 +1,6 @@ +{ + "user": "70aa272d-3413-4cda-9059-64c097956583", + "access_token": "a-valid-access-token", + "token_type": "Bearer", + "expires_in": 900 +} diff --git a/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_AwaitTokenRefresh.1.txt b/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_AwaitTokenRefresh.1.txt new file mode 100644 index 00000000000..2cf80b517ab --- /dev/null +++ b/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_AwaitTokenRefresh.1.txt @@ -0,0 +1,5 @@ +curl \ + --request POST \ + --header "Accept: application/json" \ + --cookie "zuid=some value" \ + "https://www.example.com/access?client_id=abc123" \ No newline at end of file diff --git a/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_CacheHitButItIsExpiring.1.txt b/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_CacheHitButItIsExpiring.1.txt new file mode 100644 index 00000000000..d4b93ce5daa --- /dev/null +++ b/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_CacheHitButItIsExpiring.1.txt @@ -0,0 +1,6 @@ +curl \ + --request POST \ + --header "Accept: application/json" \ + --header "Authorization: Bearer an-expiring-access-token" \ + --cookie "zuid=some value" \ + "https://www.example.com/access?client_id=abc123" \ No newline at end of file diff --git a/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_CacheIsEmpty.1.txt b/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_CacheIsEmpty.1.txt new file mode 100644 index 00000000000..2cf80b517ab --- /dev/null +++ b/WireAPI/Tests/WireAPITests/Authentication/__Snapshots__/AuthenticationManagerTests/testGetValidAccessToken_CacheIsEmpty.1.txt @@ -0,0 +1,5 @@ +curl \ + --request POST \ + --header "Accept: application/json" \ + --cookie "zuid=some value" \ + "https://www.example.com/access?client_id=abc123" \ No newline at end of file diff --git a/WireAPI/Tests/WireAPITests/Mocks/URLRequest+MockResponse.swift b/WireAPI/Tests/WireAPITests/Mocks/URLRequest+MockResponse.swift index 13bd465812e..134ed3adbd1 100644 --- a/WireAPI/Tests/WireAPITests/Mocks/URLRequest+MockResponse.swift +++ b/WireAPI/Tests/WireAPITests/Mocks/URLRequest+MockResponse.swift @@ -24,7 +24,7 @@ extension URLRequest { func mockResponse( statusCode: HTTPStatusCode, - jsonResourceName: String + jsonResourceName: String? = nil ) throws -> (Data, HTTPURLResponse) { guard let url else { throw "Unable to create mock response, request is missing url" @@ -39,8 +39,11 @@ extension URLRequest { throw "Unable to create mock response" } - let jsonPayload = HTTPClientMock.PredefinedResponse(resourceName: jsonResourceName) + guard let jsonResourceName else { + return (Data(), response) + } + let jsonPayload = HTTPClientMock.PredefinedResponse(resourceName: jsonResourceName) return (try jsonPayload.data(), response) } diff --git a/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift b/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift index 11540dac93f..6784e30da01 100644 --- a/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift +++ b/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift @@ -26,30 +26,28 @@ final class APIServiceTests: XCTestCase { var sut: APIService! var backendURL: URL! - var authenticationStorage: InMemoryAuthenticationStorage! + var authenticationManager: MockAuthenticationManagerProtocol! override func setUp() async throws { - try await super.setUp() backendURL = try XCTUnwrap(URL(string: "https://www.example.com")) - authenticationStorage = InMemoryAuthenticationStorage() + authenticationManager = MockAuthenticationManagerProtocol() let networkService = NetworkService(baseURL: backendURL) networkService.configure(with: .mockURLSession()) sut = APIService( networkService: networkService, - authenticationStorage: authenticationStorage + authenticationManager: authenticationManager ) } override func tearDown() async throws { backendURL = nil - authenticationStorage = nil + authenticationManager = nil sut = nil - try await super.tearDown() } // MARK: - Execute request - func testItExecutesARequestNotRequiringAuthentication() async throws { + func testExecuteRequest_Not_Requiring_Access_Token() async throws { // Given let request = Scaffolding.getRequest @@ -74,10 +72,10 @@ final class APIServiceTests: XCTestCase { XCTAssertEqual(receivedRequest.url?.absoluteString, backendURL.appendingPathComponent("/foo").absoluteString) } - func testItExecutesARequestRequiringAuthentication() async throws { + func testExecuteRequest_Requiring_Access_Token() async throws { // Given let request = Scaffolding.getRequest - authenticationStorage.storeAccessToken(Scaffolding.accessToken) + authenticationManager.getValidAccessToken_MockValue = Scaffolding.validAccessToken // Mock a dummy response. var receivedRequests = [URLRequest]() @@ -101,43 +99,85 @@ final class APIServiceTests: XCTestCase { // Then the request has an access token attached. let authorizationHeader = receivedRequest.value(forHTTPHeaderField: "Authorization") - XCTAssertEqual(authorizationHeader, "Bearer some-access-token") + XCTAssertEqual(authorizationHeader, "Bearer a-valid-access-token") } - func testItThrowsIfAuthenticationIsRequiredButNoAccessTokenIsFound() async throws { + func testExecuteRequest_Retry_After_First_Authentication_Error() async throws { // Given let request = Scaffolding.getRequest - XCTAssertNil(authenticationStorage.fetchAccessToken()) + authenticationManager.getValidAccessToken_MockValue = Scaffolding.validAccessToken // Mock a dummy response. - URLProtocolMock.mockHandler = { _ in - (Data(), HTTPURLResponse()) + var receivedRequests = [URLRequest]() + URLProtocolMock.mockHandler = { + receivedRequests.append($0) + return try $0.mockErrorResponse(statusCode: .unauthorized) } - // Then - await XCTAssertThrowsError(APIServiceError.missingAccessToken) { - // When - try await self.sut.executeRequest( - request, - requiringAccessToken: true - ) - } + // Mock new access token. + authenticationManager.refreshAccessToken_MockValue = Scaffolding.newAccessToken + + // When + _ = try await sut.executeRequest( + request, + requiringAccessToken: true + ) + + // Then an existing token was fetched. + XCTAssertEqual(authenticationManager.getValidAccessToken_Invocations.count, 1) + + // Then two request was received. + try XCTAssertCount(receivedRequests, count: 2) + + // Then first request has the old access token. + let firstRequest = receivedRequests[0] + XCTAssertEqual( + firstRequest.url?.absoluteString, + backendURL.appendingPathComponent("/foo").absoluteString + ) + XCTAssertEqual( + firstRequest.value(forHTTPHeaderField: "Authorization"), + "Bearer a-valid-access-token" + ) + + // Then a new token was requested. + XCTAssertEqual(authenticationManager.refreshAccessToken_Invocations.count, 1) + + // Then the second request has the new access token. + let secondRequest = receivedRequests[1] + XCTAssertEqual( + secondRequest.url?.absoluteString, + backendURL.appendingPathComponent("/foo").absoluteString + ) + XCTAssertEqual( + secondRequest.value(forHTTPHeaderField: "Authorization"), + "Bearer a-new-access-token" + ) } } private enum Scaffolding { + static let userID = UUID(uuidString: "70aa272d-3413-4cda-9059-64c097956583")! + static let getRequest = try! URLRequestBuilder(path: "/foo") .withMethod(.get) .withAcceptType(.json) .build() - static let accessToken = AccessToken( - userID: UUID(), - token: "some-access-token", + static let validAccessToken = AccessToken( + userID: userID, + token: "a-valid-access-token", + type: "Bearer", + expirationDate: Date(timeIntervalSinceNow: 900) + ) + + static let newAccessToken = AccessToken( + userID: userID, + token: "a-new-access-token", type: "Bearer", - validityInSeconds: 900 + expirationDate: Date(timeIntervalSinceNow: 900) ) } diff --git a/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift b/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift index d510190be10..3856a8fc0d6 100644 --- a/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift +++ b/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift @@ -47,7 +47,7 @@ final class NetworkServiceTests: XCTestCase { let invalidRequest = Scaffolding.invalidRequest // Then - await XCTAssertThrowsError(NetworkServiceError.invalidRequest) { + await XCTAssertThrowsErrorAsync(NetworkServiceError.invalidRequest) { // When try await self.sut.executeRequest(invalidRequest) } @@ -63,7 +63,7 @@ final class NetworkServiceTests: XCTestCase { } // Then - await XCTAssertThrowsError(NetworkServiceError.notAHTTPURLResponse) { + await XCTAssertThrowsErrorAsync(NetworkServiceError.notAHTTPURLResponse) { // When try await self.sut.executeRequest(request) } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift index fd0c2f455fc..d986c649663 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift @@ -340,7 +340,7 @@ final class TeamRepositoryTests: XCTestCase { func testStoreTeamMemberNeedsBackendUpdate_It_Throws_Error_When_Member_Was_Not_Found() async throws { // Then - await XCTAssertThrowsError { [self] in + await XCTAssertThrowsErrorAsync { [self] in // When try await sut.storeTeamMemberNeedsBackendUpdate(membershipID: Scaffolding.membershipID) } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift index e7a57cf6390..dda737df3b2 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift @@ -400,7 +400,7 @@ final class UserRepositoryTests: XCTestCase { // Then - await XCTAssertThrowsError(ConversationLabelsRepositoryError.failedToDeleteStoredLabels) { [self] in + await XCTAssertThrowsErrorAsync(ConversationLabelsRepositoryError.failedToDeleteStoredLabels) { [self] in // When diff --git a/WireFoundation/Package.swift b/WireFoundation/Package.swift index 50c233a1ec2..3b407ba6aef 100644 --- a/WireFoundation/Package.swift +++ b/WireFoundation/Package.swift @@ -9,7 +9,6 @@ let package = Package( products: [ .library(name: "WireFoundation", targets: ["WireFoundation"]), .library(name: "WireFoundationSupport", targets: ["WireFoundationSupport"]), - .library(name: "WireUtilitiesPackage", targets: ["WireUtilitiesPackage"]), .library(name: "WireTestingPackage", targets: ["WireTestingPackage"]) ], dependencies: [ @@ -29,16 +28,6 @@ let package = Package( plugins: [.plugin(name: "SourceryPlugin", package: "WirePlugins")] ), - .target( - name: "WireUtilitiesPackage", - path: "./Sources/WireUtilities" - ), - .testTarget( - name: "WireUtilitiesPackageTests", - dependencies: ["WireUtilitiesPackage"], - path: "./Tests/WireUtilitiesTests" - ), - .target( name: "WireTestingPackage", dependencies: [ diff --git a/WireFoundation/Sources/WireFoundation/Crypto/AES256Crypto.swift b/WireFoundation/Sources/WireFoundation/Crypto/AES256Crypto.swift index 50d520fdf7e..5d0b5bddb39 100644 --- a/WireFoundation/Sources/WireFoundation/Crypto/AES256Crypto.swift +++ b/WireFoundation/Sources/WireFoundation/Crypto/AES256Crypto.swift @@ -41,7 +41,7 @@ public enum AES256Crypto { /// The size (number of bytes) of the prefix. - public let prefixSize: Int + public static let prefixSize = kCCBlockSizeAES128 /// The data, including the prefix. @@ -50,14 +50,9 @@ public enum AES256Crypto { /// Create a new instance of `PrefixedData`. /// /// - Parameters: - /// - prefixSize: The size (number of bytes) of the prefix. /// - data: The data, including the prefix. - public init( - prefixSize: Int, - data: Data - ) { - self.prefixSize = prefixSize + public init(data: Data) { self.data = data } @@ -82,7 +77,7 @@ public enum AES256Crypto { plaintext: Data, key: Data ) throws -> PrefixedData { - let ivSize = kCCBlockSizeAES128 + let ivSize = PrefixedData.prefixSize let iv = try SecureRandomByteGenerator.generateBytes(count: UInt(ivSize)) let ciphertext = try encryptAllAtOnce( @@ -90,10 +85,7 @@ public enum AES256Crypto { key: key ) - return PrefixedData( - prefixSize: ivSize, - data: ciphertext - ) + return PrefixedData(data: ciphertext) } /// Decrypt data with a prefixed IV all at once. @@ -119,7 +111,7 @@ public enum AES256Crypto { key: key ) - return plaintext.dropFirst(ciphertext.prefixSize) + return plaintext.dropFirst(PrefixedData.prefixSize) } // MARK: - Plain old encrypt / decrypt diff --git a/WireFoundation/Sources/WireFoundation/SDKAbstractions/Keychain.swift b/WireFoundation/Sources/WireFoundation/SDKAbstractions/Keychain.swift new file mode 100644 index 00000000000..e9ba789841e --- /dev/null +++ b/WireFoundation/Sources/WireFoundation/SDKAbstractions/Keychain.swift @@ -0,0 +1,120 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +public import Foundation + +/// A simple wrapper around the Keychain api. + +public struct Keychain: KeychainProtocol { + + /// Add one or more items to a keychain. + /// + /// For more information, refer to the documentation of `SecItemAdd`. + + public func addItem( + query: Set + ) async throws { + let status = SecItemAdd( + query.toCFDictionary(), + nil + ) + + guard status == errSecSuccess else { + throw KeychainError.errorStatus(status) + } + } + + /// Modify zero or more items which match a search query. + /// + /// For more information, refer to the documentation of `SecItemUpdate`. + + public func updateItem( + query: Set, + attributesToUpdate: Set + ) async throws { + let status = SecItemUpdate( + query.toCFDictionary(), + attributesToUpdate.toCFDictionary() + ) + + guard status == errSecSuccess else { + throw KeychainError.errorStatus(status) + } + } + + /// Returns one or more items which match a search query. + /// + /// For more information, refer to the documentation of `SecItemCopyMatching`. + + public func fetchItem( + query: Set + ) async throws -> T? { + var result: CFTypeRef? + + let status = SecItemCopyMatching( + query.toCFDictionary(), + &result + ) + + if status == errSecItemNotFound { + return nil + } + + guard status == errSecSuccess else { + throw KeychainError.errorStatus(status) + } + + guard let castedResult = result as? T else { + throw KeychainError.failedToCastResult + } + + return castedResult + } + + /// Delete zero or more items which match a search query. + /// + /// For more information, refer to the documentation of `SecItemDelete`. + + public func deleteItem( + query: Set + ) async throws { + let status = SecItemDelete( + query.toCFDictionary() + ) + + guard status == errSecSuccess else { + throw KeychainError.errorStatus(status) + } + } + +} + +private extension Set { + + func toCFDictionary() -> CFDictionary { + var dictionary = [CFString: Any]() + + for item in self { + let entry = item.toCFDictionaryEntry() + dictionary[entry.0] = entry.1 + } + + return dictionary as CFDictionary + } + +} diff --git a/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift b/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift new file mode 100644 index 00000000000..3e381e8194d --- /dev/null +++ b/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift @@ -0,0 +1,93 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +public import Foundation + +/// A protocol mirroring Keychain api to allow mocking in tests. +public protocol KeychainProtocol: Sendable { + + func addItem( + query: Set + ) async throws + + func updateItem( + query: Set, + attributesToUpdate: Set + ) async throws + + func fetchItem( + query: Set + ) async throws -> T? + + func deleteItem( + query: Set + ) async throws + +} + +public enum KeychainError: Error { + + case failedToCastResult + case errorStatus(OSStatus) + +} + +public enum KeychainQueryItem: Hashable, Equatable, Sendable { + + case service(String) + case account(String) + case itemClass(ItemClass) + case accessible(ItemAccessibility) + case returningData(Bool) + case data(Data) + + public enum ItemClass: Equatable, Sendable { + + case genericPassword + + } + + public enum ItemAccessibility: Equatable, Sendable { + + case afterFirstUnlock + + } + + func toCFDictionaryEntry() -> (CFString, Any) { + switch self { + case let .service(string): + (kSecAttrService, string) + + case let .account(string): + (kSecAttrAccount, string) + + case let .itemClass(itemClass): + (kSecClass, itemClass) + + case .accessible(.afterFirstUnlock): + (kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock) + + case let .returningData(bool): + (kSecReturnData, bool) + + case let .data(data): + (kSecValueData, data) + } + } + +} diff --git a/WireFoundation/Sources/WireFoundationSupport/Sourcery/AutoMockable.manual.swift b/WireFoundation/Sources/WireFoundationSupport/Sourcery/AutoMockable.manual.swift new file mode 100644 index 00000000000..dc020a3367e --- /dev/null +++ b/WireFoundation/Sources/WireFoundationSupport/Sourcery/AutoMockable.manual.swift @@ -0,0 +1,145 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +public import WireFoundation + +public actor MockKeychainProtocol: KeychainProtocol { + + // MARK: - addItem + + public var addItemQuery_Invocations: [Set] = [] + + var addItemQuery_MockError: (any Error)? + public func setAddItemQuery_MockError(_ error: any Error) { + addItemQuery_MockError = error + } + + var addItemQuery_MockMethod: ((Set) async throws -> Void)? + public func setAddItemQuery_MockMethod(_ method: @escaping (Set) async throws -> Void) { + addItemQuery_MockMethod = method + } + + public func addItem(query: Set) async throws { + addItemQuery_Invocations.append(query) + + if let error = addItemQuery_MockError { + throw error + } + + guard let mock = addItemQuery_MockMethod else { + fatalError("no mock for `addItemQuery`") + } + + try await mock(query) + } + + // MARK: - updateItem + + public var updateItemQueryAttributesToUpdate_Invocations: [(query: Set, attributesToUpdate: Set)] = [] + + var updateItemQueryAttributesToUpdate_MockError: (any Error)? + public func setUpdateItemQueryAttributesToUpdate_MockError(_ error: any Error) { + updateItemQueryAttributesToUpdate_MockError = error + } + + var updateItemQueryAttributesToUpdate_MockMethod: ((Set, Set) async throws -> Void)? + public func setUpdateItemQueryAttributesToUpdate_MockMethod(_ method: @escaping (Set, Set) async throws -> Void) { + updateItemQueryAttributesToUpdate_MockMethod = method + } + + public func updateItem(query: Set, attributesToUpdate: Set) async throws { + updateItemQueryAttributesToUpdate_Invocations.append((query: query, attributesToUpdate: attributesToUpdate)) + + if let error = updateItemQueryAttributesToUpdate_MockError { + throw error + } + + guard let mock = updateItemQueryAttributesToUpdate_MockMethod else { + fatalError("no mock for `updateItemQueryAttributesToUpdate`") + } + + try await mock(query, attributesToUpdate) + } + + // MARK: - fetchItem + + public var fetchItemQuery_Invocations: [Set] = [] + + var fetchItemQuery_MockError: (any Error)? + public func setFetchItemQuery_MockError(_ error: any Error) { + fetchItemQuery_MockError = error + } + + var fetchItemQuery_MockMethod: ((Set) async throws -> (any Sendable)?)? + public func setFetchItemQuery_MockMethod( + _ method: @escaping (Set) async throws -> (any Sendable)? + ) { + fetchItemQuery_MockMethod = method + } + + var fetchItemQuery_MockValue: (any Sendable)?? + public func setFetchItemQuery_MockValue(_ value: (any Sendable)?) async { + fetchItemQuery_MockValue = value + } + + public func fetchItem(query: Set) async throws -> T? { + fetchItemQuery_Invocations.append(query) + + if let error = fetchItemQuery_MockError { + throw error + } + + if let mock = fetchItemQuery_MockMethod { + return try await mock(query) as? T + } else if let mock = fetchItemQuery_MockValue { + return mock as? T + } else { + fatalError("no mock for `fetchItemQuery`") + } + } + + // MARK: - deleteItem + + public var deleteItemQuery_Invocations: [Set] = [] + + var deleteItemQuery_MockError: (any Error)? + public func setDeleteItemQuery_MockError(_ error: any Error) { + deleteItemQuery_MockError = error + } + + var deleteItemQuery_MockMethod: ((Set) async throws -> Void)? + public func setDeleteItemQuery_MockMethod(_ method: @escaping (Set) async throws -> Void) { + deleteItemQuery_MockMethod = method + } + + public func deleteItem(query: Set) async throws { + deleteItemQuery_Invocations.append(query) + + if let error = deleteItemQuery_MockError { + throw error + } + + guard let mock = deleteItemQuery_MockMethod else { + fatalError("no mock for `deleteItemQuery`") + } + + try await mock(query) + } + +} diff --git a/WireFoundation/Sources/WireSystemSupport/Sourcery/AutoMockable.stencil b/WireFoundation/Sources/WireSystemSupport/Sourcery/AutoMockable.stencil deleted file mode 120000 index 384e2627f10..00000000000 --- a/WireFoundation/Sources/WireSystemSupport/Sourcery/AutoMockable.stencil +++ /dev/null @@ -1 +0,0 @@ -../../../../WirePlugins/Plugins/SourceryPlugin/Stencils/AutoMockable.stencil \ No newline at end of file diff --git a/WireFoundation/Sources/WireSystemSupport/Sourcery/sourcery.yml b/WireFoundation/Sources/WireSystemSupport/Sourcery/sourcery.yml deleted file mode 100644 index d208cd691c9..00000000000 --- a/WireFoundation/Sources/WireSystemSupport/Sourcery/sourcery.yml +++ /dev/null @@ -1,8 +0,0 @@ -sources: -- ${PACKAGE_ROOT_DIR}/Sources/WireSystem -templates: -- ${TARGET_DIR}/Sourcery/AutoMockable.stencil -output: - ${DERIVED_SOURCES_DIR} -args: - autoMockableImports: ["WireSystem"] diff --git a/WireFoundation/Sources/WireSystemSupport/WireSystem.swift b/WireFoundation/Sources/WireSystemSupport/WireSystem.swift deleted file mode 100644 index 1861c8dd67b..00000000000 --- a/WireFoundation/Sources/WireSystemSupport/WireSystem.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Wire -// Copyright (C) 2024 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -// This target generates mocks via 'sourcery'. It uses the plugin configured in `Package.swift`. -// The generated mocks are processed from the sandbox directory and are not visible in the project folder: -// https://github.com/apple/swift-package-manager/blob/main/Documentation/Plugins.md#implementing-the-build-tool-plugin-script diff --git a/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestCase+Assertions.swift b/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestCase+Assertions.swift index 53ac0d1d85b..4349f927a02 100644 --- a/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestCase+Assertions.swift +++ b/WireFoundation/Sources/WireTesting/SDKExtensions/XCTest/XCTestCase+Assertions.swift @@ -59,14 +59,14 @@ public extension XCTestCase { /// - file: The file name of the invoking test. /// - line: The line number when this assertion is made. - func XCTAssertThrowsError( + func XCTAssertThrowsErrorAsync( _ expectedError: E, when expression: @escaping () async throws -> some Any, _ message: String? = nil, file: StaticString = #filePath, line: UInt = #line ) async { - await XCTAssertThrowsError( + await XCTAssertThrowsErrorAsync( expression, message, file: file, @@ -98,12 +98,12 @@ public extension XCTestCase { /// - line: The line number when this assertion is made. /// - errorHandler: A handler for the thrown error. - func XCTAssertThrowsError( + func XCTAssertThrowsErrorAsync( _ expression: () async throws -> some Any, _ message: String? = nil, file: StaticString = #filePath, line: UInt = #line, - _ errorHandler: (_ error: any Error) -> Void = { _ in } + errorHandler: (_ error: any Error) -> Void = { _ in } ) async { do { _ = try await expression() diff --git a/WireFoundation/Sources/WireUtilities/Documentation.docc/Documentation.md b/WireFoundation/Sources/WireUtilities/Documentation.docc/Documentation.md deleted file mode 100644 index a1053da9ee1..00000000000 --- a/WireFoundation/Sources/WireUtilities/Documentation.docc/Documentation.md +++ /dev/null @@ -1,9 +0,0 @@ -# ``WireUtilities`` - -Provides various utility helpers. - -## Overview - -WireUtilities contains several helper functions and objects. - -## Topics diff --git a/WireFoundation/Sources/WireUtilities/Placeholder.swift b/WireFoundation/Sources/WireUtilities/Placeholder.swift deleted file mode 100644 index 5e303c2201a..00000000000 --- a/WireFoundation/Sources/WireUtilities/Placeholder.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Wire -// Copyright (C) 2024 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// diff --git a/WireFoundation/Tests/WireFoundationTests/Crypto/AES256CryptoTests.swift b/WireFoundation/Tests/WireFoundationTests/Crypto/AES256CryptoTests.swift index eb4a38ab07b..3c41ad4ccbd 100644 --- a/WireFoundation/Tests/WireFoundationTests/Crypto/AES256CryptoTests.swift +++ b/WireFoundation/Tests/WireFoundationTests/Crypto/AES256CryptoTests.swift @@ -196,7 +196,7 @@ final class AES256CryptoTests: XCTestCase { let key = try Scaffolding.randomInvalidKey() // Then - await XCTAssertThrowsError(AES256CryptoError.invalidKeyLength) { + await XCTAssertThrowsErrorAsync(AES256CryptoError.invalidKeyLength) { // When try AES256Crypto.encryptAllAtOnce( plaintext: originalData, @@ -211,7 +211,7 @@ final class AES256CryptoTests: XCTestCase { let key = try Scaffolding.randomInvalidKey() // Then - await XCTAssertThrowsError(AES256CryptoError.invalidKeyLength) { + await XCTAssertThrowsErrorAsync(AES256CryptoError.invalidKeyLength) { // When try AES256Crypto.decryptAllAtOnce( ciphertext: originalData, diff --git a/WireUI/Sources/WireFolderPickerUI/Resources/Localizable.xcstrings b/WireUI/Sources/WireFolderPickerUI/Resources/Localizable.xcstrings index 37460ae8102..e632b1855e7 100644 --- a/WireUI/Sources/WireFolderPickerUI/Resources/Localizable.xcstrings +++ b/WireUI/Sources/WireFolderPickerUI/Resources/Localizable.xcstrings @@ -129,11 +129,125 @@ "folderPicker.emptyState.link.text" : { "comment" : "The link text for documentation on how to add a conversation to a folder", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "معرفة المزيد" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lær mere" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr erfahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Learn more" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Más información" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loe lähemalt" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lue lisää" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En savoir plus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulteriori informazioni" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "もっと知る" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sužinoti daugiau" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leer meer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Więcej informacji" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mais informação" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подробнее" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nauči se več" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha fazla bilgi" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дізнатися більше" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更多信息" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "瞭解詳情" + } } } }, @@ -264,4 +378,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/WireUI/Sources/WireMoveToFolderUI/Resources/Accessibility.xcstrings b/WireUI/Sources/WireMoveToFolderUI/Resources/Accessibility.xcstrings index 2d447128fa0..730e3858e1f 100644 --- a/WireUI/Sources/WireMoveToFolderUI/Resources/Accessibility.xcstrings +++ b/WireUI/Sources/WireMoveToFolderUI/Resources/Accessibility.xcstrings @@ -3,14 +3,128 @@ "strings" : { "folderPicker.close.label" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ordnerübersicht schließen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Close folder overview" } + }, + "es" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "et" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "fr" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "it" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "ja" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "lt" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "pl" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть обзор папки" + } + }, + "sl" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "tr" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "uk" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "new", + "value" : "Close folder overview" + } } } } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/WireUI/Sources/WireMoveToFolderUI/Resources/Localizable.xcstrings b/WireUI/Sources/WireMoveToFolderUI/Resources/Localizable.xcstrings index da6367b80b4..863e7be76e6 100644 --- a/WireUI/Sources/WireMoveToFolderUI/Resources/Localizable.xcstrings +++ b/WireUI/Sources/WireMoveToFolderUI/Resources/Localizable.xcstrings @@ -4,69 +4,753 @@ "folder.creation.name.button.create" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إنشاء" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opret" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erstellen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Create" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Créer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Creare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規作成" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurti" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aanmaken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utwórz" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Criar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Создать" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustvari" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oluştur" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Створити" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立" + } } } }, "folder.creation.name.footer" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "Maximum 64 characters" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "Maximum 64 characters" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximal 64 Zeichen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Maximum 64 characters" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máximo de 64 caracteres" + } + }, + "et" : { + "stringUnit" : { + "state" : "new", + "value" : "Maximum 64 characters" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "Maximum 64 characters" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "64 caractères maximum" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Massimo 64 caratteri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大64文字" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iki 64 simbolių" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Maximum 64 characters" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksymalnie 64 znaki" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máximo: 64 caracteres" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не более 64 символов" + } + }, + "sl" : { + "stringUnit" : { + "state" : "new", + "value" : "Maximum 64 characters" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En fazla 64 karakter" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимум 64 символи" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最多64个字符" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "最多64個字元" + } } } }, "folder.creation.name.header" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterhaltung %@ in einen neuen Ordner verschieben" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Move the conversation %@ to a new folder" } + }, + "es" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "et" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "fr" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "it" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "ja" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "lt" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "pl" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переместить беседу с %@ в новую папку." + } + }, + "sl" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "tr" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "uk" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "new", + "value" : "Move the conversation %@ to a new folder" + } } } }, "folder.creation.name.title" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "Create new folder" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "Create new folder" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neuen Ordner anlegen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Create new folder" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear nueva carpeta" + } + }, + "et" : { + "stringUnit" : { + "state" : "new", + "value" : "Create new folder" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "Create new folder" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Créer un nouveau dossier" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crea nuova cartella" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新規フォルダを作成" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sukurti naują aplanką" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Create new folder" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utwórz nowy folder" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Criar nova pasta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Создание новой папки" + } + }, + "sl" : { + "stringUnit" : { + "state" : "new", + "value" : "Create new folder" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni klasör oluştur" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Створити нову теку" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "建立新资料夹" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建資料夾" + } } } }, "folder.picker.empty.hint" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "انشئ مجلدا جديدا بالضغط على زر +" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "Create a new folder by pressing the + button" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neuen Ordner durch Drücken der + Taste anlegen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Create a new folder by pressing the + button" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear nueva carpeta presionando el botón +" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loo uus kaust, vajutades + nuppu" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "Create a new folder by pressing the + button" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Créer un nouveau dossier en cliquant sur the bouton +" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crea una nuova cartella premendo il pulsante +" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ボタンを押して、新しいフォルダを作成します。" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naują aplanką sukursite paspaudę + mygtuką" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Create a new folder by pressing the + button" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utwórz nowy folder naciskając przycisk +" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Criar uma nova pasta pressionando o botão +" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Создайте новую папку кнопкой +" + } + }, + "sl" : { + "stringUnit" : { + "state" : "new", + "value" : "Create a new folder by pressing the + button" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ Düğmesine basarak yeni bir klasör oluşturun" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Створити нову теку за допомогою кнопки +" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "按 + 按钮创建新文件夹" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按+按鈕創建一個新文件夾" + } } } }, "folder.picker.title" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschieben nach" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Move To" } + }, + "es" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "et" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "fr" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "it" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "ja" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "lt" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "pl" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переместить в…" + } + }, + "sl" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "tr" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "uk" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "new", + "value" : "Move To" + } } } } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/WireFoundation/Sources/WireSystem/Cache.swift b/wire-ios-system/Source/Cache.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Cache.swift rename to wire-ios-system/Source/Cache.swift diff --git a/WireFoundation/Sources/WireSystem/CircularArray.swift b/wire-ios-system/Source/CircularArray.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/CircularArray.swift rename to wire-ios-system/Source/CircularArray.swift diff --git a/WireFoundation/Sources/WireSystem/DateProviding/CurrentDateProviding.swift b/wire-ios-system/Source/DateProviding/CurrentDateProviding.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/DateProviding/CurrentDateProviding.swift rename to wire-ios-system/Source/DateProviding/CurrentDateProviding.swift diff --git a/WireFoundation/Sources/WireSystem/DateProviding/SystemDateProvider.swift b/wire-ios-system/Source/DateProviding/SystemDateProvider.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/DateProviding/SystemDateProvider.swift rename to wire-ios-system/Source/DateProviding/SystemDateProvider.swift diff --git a/WireFoundation/Sources/WireSystem/DispatchQueue+ZMSDispatchGroup.swift b/wire-ios-system/Source/DispatchQueue+ZMSDispatchGroup.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/DispatchQueue+ZMSDispatchGroup.swift rename to wire-ios-system/Source/DispatchQueue+ZMSDispatchGroup.swift diff --git a/WireFoundation/Sources/WireSystem/ExpiringActivity.swift b/wire-ios-system/Source/ExpiringActivity.swift similarity index 99% rename from WireFoundation/Sources/WireSystem/ExpiringActivity.swift rename to wire-ios-system/Source/ExpiringActivity.swift index 87cc24bf33a..d33ab167733 100644 --- a/WireFoundation/Sources/WireSystem/ExpiringActivity.swift +++ b/wire-ios-system/Source/ExpiringActivity.swift @@ -17,6 +17,7 @@ // import Foundation +import WireFoundation protocol ExpiringActivityInterface { diff --git a/WireFoundation/Sources/WireSystem/Extensions/Foundation/Error+SafeForLoggingStringConvertible.swift b/wire-ios-system/Source/Extensions/Foundation/Error+SafeForLoggingStringConvertible.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Extensions/Foundation/Error+SafeForLoggingStringConvertible.swift rename to wire-ios-system/Source/Extensions/Foundation/Error+SafeForLoggingStringConvertible.swift diff --git a/WireFoundation/Sources/WireSystem/Extensions/Foundation/NSAttributedStringExtensions.swift b/wire-ios-system/Source/Extensions/Foundation/NSAttributedStringExtensions.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Extensions/Foundation/NSAttributedStringExtensions.swift rename to wire-ios-system/Source/Extensions/Foundation/NSAttributedStringExtensions.swift diff --git a/WireFoundation/Sources/WireSystem/Extensions/UIKit/UIModalPresentationStyle+CustomDebugStringConvertible.swift b/wire-ios-system/Source/Extensions/UIKit/UIModalPresentationStyle+CustomDebugStringConvertible.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Extensions/UIKit/UIModalPresentationStyle+CustomDebugStringConvertible.swift rename to wire-ios-system/Source/Extensions/UIKit/UIModalPresentationStyle+CustomDebugStringConvertible.swift diff --git a/WireFoundation/Sources/WireSystem/GroupQueue.swift b/wire-ios-system/Source/GroupQueue.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/GroupQueue.swift rename to wire-ios-system/Source/GroupQueue.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/AggregatedLogger.swift b/wire-ios-system/Source/Logging/AggregatedLogger.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/AggregatedLogger.swift rename to wire-ios-system/Source/Logging/AggregatedLogger.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/CocoaLumberjackLogger.swift b/wire-ios-system/Source/Logging/CocoaLumberjackLogger.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/CocoaLumberjackLogger.swift rename to wire-ios-system/Source/Logging/CocoaLumberjackLogger.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/Flow.swift b/wire-ios-system/Source/Logging/Flow.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/Flow.swift rename to wire-ios-system/Source/Logging/Flow.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/LogAttributes.swift b/wire-ios-system/Source/Logging/LogAttributes.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/LogAttributes.swift rename to wire-ios-system/Source/Logging/LogAttributes.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/LogConvertible.swift b/wire-ios-system/Source/Logging/LogConvertible.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/LogConvertible.swift rename to wire-ios-system/Source/Logging/LogConvertible.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/LoggerProtocol.swift b/wire-ios-system/Source/Logging/LoggerProtocol.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/LoggerProtocol.swift rename to wire-ios-system/Source/Logging/LoggerProtocol.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/SystemLogger.swift b/wire-ios-system/Source/Logging/SystemLogger.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/SystemLogger.swift rename to wire-ios-system/Source/Logging/SystemLogger.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/WireLogger+Instances.swift b/wire-ios-system/Source/Logging/WireLogger+Instances.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/WireLogger+Instances.swift rename to wire-ios-system/Source/Logging/WireLogger+Instances.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/WireLogger.swift b/wire-ios-system/Source/Logging/WireLogger.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/WireLogger.swift rename to wire-ios-system/Source/Logging/WireLogger.swift diff --git a/WireFoundation/Sources/WireSystem/Logging/WireLoggerObjc.swift b/wire-ios-system/Source/Logging/WireLoggerObjc.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/Logging/WireLoggerObjc.swift rename to wire-ios-system/Source/Logging/WireLoggerObjc.swift diff --git a/WireFoundation/Sources/WireSystem/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegate.swift b/wire-ios-system/Source/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegate.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegate.swift rename to wire-ios-system/Source/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegate.swift diff --git a/WireFoundation/Sources/WireSystem/PopoverPresentationControllerConfiguration.swift b/wire-ios-system/Source/PopoverPresentationControllerConfiguration.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/PopoverPresentationControllerConfiguration.swift rename to wire-ios-system/Source/PopoverPresentationControllerConfiguration.swift diff --git a/WireFoundation/Sources/WireSystem/SafeForLoggingStringConvertible.swift b/wire-ios-system/Source/SafeForLoggingStringConvertible.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/SafeForLoggingStringConvertible.swift rename to wire-ios-system/Source/SafeForLoggingStringConvertible.swift diff --git a/WireFoundation/Sources/WireSystem/SanitizedString.swift b/wire-ios-system/Source/SanitizedString.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/SanitizedString.swift rename to wire-ios-system/Source/SanitizedString.swift diff --git a/WireFoundation/Sources/WireSystem/TimePoint.swift b/wire-ios-system/Source/TimePoint.swift similarity index 99% rename from WireFoundation/Sources/WireSystem/TimePoint.swift rename to wire-ios-system/Source/TimePoint.swift index 667f023b62a..d803eb127eb 100644 --- a/WireFoundation/Sources/WireSystem/TimePoint.swift +++ b/wire-ios-system/Source/TimePoint.swift @@ -17,6 +17,7 @@ // import Foundation +import WireFoundation /// Records the passage of time since its creation. It also stores the callstack at creation time. @objc(ZMSTimePoint) @objcMembers diff --git a/WireFoundation/Sources/WireSystem/UserDefaults+Temporary.swift b/wire-ios-system/Source/UserDefaults+Temporary.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/UserDefaults+Temporary.swift rename to wire-ios-system/Source/UserDefaults+Temporary.swift diff --git a/WireFoundation/Sources/WireSystem/WireSystem.docc/WireSystem.md b/wire-ios-system/Source/WireSystem.docc/WireSystem.md similarity index 100% rename from WireFoundation/Sources/WireSystem/WireSystem.docc/WireSystem.md rename to wire-ios-system/Source/WireSystem.docc/WireSystem.md diff --git a/WireFoundation/Sources/WireSystem/ZMAssertionDumpFile.swift b/wire-ios-system/Source/ZMAssertionDumpFile.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/ZMAssertionDumpFile.swift rename to wire-ios-system/Source/ZMAssertionDumpFile.swift diff --git a/WireFoundation/Sources/WireSystem/ZMLogLevel.swift b/wire-ios-system/Source/ZMLogLevel.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/ZMLogLevel.swift rename to wire-ios-system/Source/ZMLogLevel.swift diff --git a/WireFoundation/Sources/WireSystem/ZMSAsserts.swift b/wire-ios-system/Source/ZMSAsserts.swift similarity index 99% rename from WireFoundation/Sources/WireSystem/ZMSAsserts.swift rename to wire-ios-system/Source/ZMSAsserts.swift index bed44c9e836..4edb7a4e3e2 100644 --- a/WireFoundation/Sources/WireSystem/ZMSAsserts.swift +++ b/wire-ios-system/Source/ZMSAsserts.swift @@ -17,6 +17,7 @@ // import Foundation +import WireFoundation /// Reports an error and terminates the application public func fatal( diff --git a/WireFoundation/Sources/WireSystem/ZMSDispatchGroup.swift b/wire-ios-system/Source/ZMSDispatchGroup.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/ZMSDispatchGroup.swift rename to wire-ios-system/Source/ZMSDispatchGroup.swift diff --git a/WireFoundation/Sources/WireSystem/ZMSLog+Levels.swift b/wire-ios-system/Source/ZMSLog+Levels.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/ZMSLog+Levels.swift rename to wire-ios-system/Source/ZMSLog+Levels.swift diff --git a/WireFoundation/Sources/WireSystem/ZMSLog+Recording.swift b/wire-ios-system/Source/ZMSLog+Recording.swift similarity index 100% rename from WireFoundation/Sources/WireSystem/ZMSLog+Recording.swift rename to wire-ios-system/Source/ZMSLog+Recording.swift diff --git a/WireFoundation/Sources/WireSystem/ZMSLog.swift b/wire-ios-system/Source/ZMSLog.swift similarity index 99% rename from WireFoundation/Sources/WireSystem/ZMSLog.swift rename to wire-ios-system/Source/ZMSLog.swift index d67e7e0b3be..413edbfaef3 100644 --- a/WireFoundation/Sources/WireSystem/ZMSLog.swift +++ b/wire-ios-system/Source/ZMSLog.swift @@ -19,6 +19,7 @@ import Foundation import os.log import ZipArchive +import WireFoundation /// Represents an entry to be logged. @objcMembers diff --git a/wire-ios-system/Support/Sourcery/config.yml b/wire-ios-system/Support/Sourcery/config.yml index c87a58da7f3..9680263856e 100644 --- a/wire-ios-system/Support/Sourcery/config.yml +++ b/wire-ios-system/Support/Sourcery/config.yml @@ -1,5 +1,5 @@ sources: - - ../../../WireFoundation/Sources/WireSystem + - ../../Source templates: - ./AutoMockable.stencil output: diff --git a/WireFoundation/Tests/WireSystemTests/CacheTests.swift b/wire-ios-system/Tests/CacheTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/CacheTests.swift rename to wire-ios-system/Tests/CacheTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/CircularArrayTests.swift b/wire-ios-system/Tests/CircularArrayTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/CircularArrayTests.swift rename to wire-ios-system/Tests/CircularArrayTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/DispatchGroupTests.swift b/wire-ios-system/Tests/DispatchGroupTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/DispatchGroupTests.swift rename to wire-ios-system/Tests/DispatchGroupTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/DispatchQueueHelperTests.swift b/wire-ios-system/Tests/DispatchQueueHelperTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/DispatchQueueHelperTests.swift rename to wire-ios-system/Tests/DispatchQueueHelperTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/ExpiringActivityTests.swift b/wire-ios-system/Tests/ExpiringActivityTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/ExpiringActivityTests.swift rename to wire-ios-system/Tests/ExpiringActivityTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegateTests.swift b/wire-ios-system/Tests/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegateTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegateTests.swift rename to wire-ios-system/Tests/NavigationControllerDelegate/SupportedOrientationsDelegatingNavigationControllerDelegateTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/PopoverPresentationControllerConfigurationTests.swift b/wire-ios-system/Tests/PopoverPresentationControllerConfigurationTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/PopoverPresentationControllerConfigurationTests.swift rename to wire-ios-system/Tests/PopoverPresentationControllerConfigurationTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/SanitizedStringTests.swift b/wire-ios-system/Tests/SanitizedStringTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/SanitizedStringTests.swift rename to wire-ios-system/Tests/SanitizedStringTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/TimePointTests.swift b/wire-ios-system/Tests/TimePointTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/TimePointTests.swift rename to wire-ios-system/Tests/TimePointTests.swift diff --git a/WireFoundation/Tests/WireSystemTests/ZMAssertionDumpFileTests.swift b/wire-ios-system/Tests/ZMAssertionDumpFileTests.swift similarity index 100% rename from WireFoundation/Tests/WireSystemTests/ZMAssertionDumpFileTests.swift rename to wire-ios-system/Tests/ZMAssertionDumpFileTests.swift diff --git a/wire-ios-system/Tests/ZMDefinesTest.m b/wire-ios-system/Tests/ZMDefinesTest.m index 2d4dbbca470..5b28aa02ec9 100644 --- a/wire-ios-system/Tests/ZMDefinesTest.m +++ b/wire-ios-system/Tests/ZMDefinesTest.m @@ -16,6 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +@import WireFoundation; @import WireSystem; @import XCTest; diff --git a/WireFoundation/Tests/WireSystemTests/ZMLogTests.swift b/wire-ios-system/Tests/ZMLogTests.swift similarity index 99% rename from WireFoundation/Tests/WireSystemTests/ZMLogTests.swift rename to wire-ios-system/Tests/ZMLogTests.swift index 6c86ffbe5f7..fccae4b1715 100644 --- a/WireFoundation/Tests/WireSystemTests/ZMLogTests.swift +++ b/wire-ios-system/Tests/ZMLogTests.swift @@ -17,6 +17,7 @@ // import XCTest + @testable import WireSystem class ZMLogTests: XCTestCase { diff --git a/wire-ios-system/WireSystem.docc/WireSystem.md b/wire-ios-system/WireSystem.docc/WireSystem.md new file mode 100644 index 00000000000..e54ac289986 --- /dev/null +++ b/wire-ios-system/WireSystem.docc/WireSystem.md @@ -0,0 +1,9 @@ +# ``WireSystem`` + +Provide core shared code. + +## Overview + +WireSystem provides some core shared code. + +## Topics diff --git a/wire-ios-system/WireSystem.xcodeproj/project.pbxproj b/wire-ios-system/WireSystem.xcodeproj/project.pbxproj index c55ea9da900..19add7131b5 100644 --- a/wire-ios-system/WireSystem.xcodeproj/project.pbxproj +++ b/wire-ios-system/WireSystem.xcodeproj/project.pbxproj @@ -7,44 +7,12 @@ objects = { /* Begin PBXBuildFile section */ - 013334BC2C204AB0002D97DB /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 013334BB2C204AB0002D97DB /* CocoaLumberjackSwift */; }; 591B6E892C8B0A33009F8A7B /* ZipArchive.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6B58E402B21D8920046B6E1 /* ZipArchive.xcframework */; }; 591B6E8C2C8B0A37009F8A7B /* WireSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3ECC35191AD436750089FD4B /* WireSystem.framework */; }; 591B6E8F2C8B0A3A009F8A7B /* WireSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3ECC35191AD436750089FD4B /* WireSystem.framework */; }; - 5939CFCE2CECAF3D000F6FAD /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC02CECAF3D000F6FAD /* CacheTests.swift */; }; - 5939CFCF2CECAF3D000F6FAD /* CircularArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC12CECAF3D000F6FAD /* CircularArrayTests.swift */; }; - 5939CFD02CECAF3D000F6FAD /* DispatchGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC22CECAF3D000F6FAD /* DispatchGroupTests.swift */; }; - 5939CFD12CECAF3D000F6FAD /* DispatchQueueHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC32CECAF3D000F6FAD /* DispatchQueueHelperTests.swift */; }; - 5939CFD22CECAF3D000F6FAD /* ExpiringActivityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC42CECAF3D000F6FAD /* ExpiringActivityTests.swift */; }; - 5939CFD32CECAF3D000F6FAD /* PopoverPresentationControllerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC52CECAF3D000F6FAD /* PopoverPresentationControllerConfigurationTests.swift */; }; - 5939CFD42CECAF3D000F6FAD /* SanitizedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC62CECAF3D000F6FAD /* SanitizedStringTests.swift */; }; - 5939CFD52CECAF3D000F6FAD /* TimePointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC82CECAF3D000F6FAD /* TimePointTests.swift */; }; - 5939CFD62CECAF3D000F6FAD /* ZMAssertionDumpFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFC92CECAF3D000F6FAD /* ZMAssertionDumpFileTests.swift */; }; - 5939CFD72CECAF3D000F6FAD /* ZMDefinesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFCA2CECAF3D000F6FAD /* ZMDefinesTest.m */; }; - 5939CFD82CECAF3D000F6FAD /* ZMLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939CFCB2CECAF3D000F6FAD /* ZMLogTests.swift */; }; - 5939D12A2CECB0B5000F6FAD /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1042CECB0B5000F6FAD /* Cache.swift */; }; - 5939D12B2CECB0B5000F6FAD /* CircularArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1052CECB0B5000F6FAD /* CircularArray.swift */; }; - 5939D12C2CECB0B5000F6FAD /* DispatchQueue+ZMSDispatchGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1062CECB0B5000F6FAD /* DispatchQueue+ZMSDispatchGroup.swift */; }; - 5939D12D2CECB0B5000F6FAD /* ExpiringActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1072CECB0B5000F6FAD /* ExpiringActivity.swift */; }; - 5939D12E2CECB0B5000F6FAD /* GroupQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1082CECB0B5000F6FAD /* GroupQueue.swift */; }; - 5939D12F2CECB0B5000F6FAD /* PopoverPresentationControllerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1092CECB0B5000F6FAD /* PopoverPresentationControllerConfiguration.swift */; }; - 5939D1302CECB0B5000F6FAD /* SafeForLoggingStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D10A2CECB0B5000F6FAD /* SafeForLoggingStringConvertible.swift */; }; - 5939D1312CECB0B5000F6FAD /* SanitizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D10B2CECB0B5000F6FAD /* SanitizedString.swift */; }; - 5939D1322CECB0B5000F6FAD /* TimePoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D10C2CECB0B5000F6FAD /* TimePoint.swift */; }; - 5939D1332CECB0B5000F6FAD /* UserDefaults+Temporary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D10D2CECB0B5000F6FAD /* UserDefaults+Temporary.swift */; }; - 5939D1342CECB0B5000F6FAD /* ZMAssertionDumpFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D10F2CECB0B5000F6FAD /* ZMAssertionDumpFile.swift */; }; - 5939D1352CECB0B5000F6FAD /* ZMLogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1102CECB0B5000F6FAD /* ZMLogLevel.swift */; }; - 5939D1362CECB0B5000F6FAD /* ZMSAsserts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1122CECB0B5000F6FAD /* ZMSAsserts.swift */; }; - 5939D1372CECB0B5000F6FAD /* ZMSDispatchGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1142CECB0B5000F6FAD /* ZMSDispatchGroup.swift */; }; - 5939D1382CECB0B5000F6FAD /* ZMSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1152CECB0B5000F6FAD /* ZMSLog.swift */; }; - 5939D1392CECB0B5000F6FAD /* ZMSLog+Levels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1162CECB0B5000F6FAD /* ZMSLog+Levels.swift */; }; - 5939D13A2CECB0B5000F6FAD /* ZMSLog+Recording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5939D1172CECB0B5000F6FAD /* ZMSLog+Recording.swift */; }; - 5939D13B2CECB0B5000F6FAD /* WireSystem.h in Headers */ = {isa = PBXBuildFile; fileRef = 5939D10E2CECB0B5000F6FAD /* WireSystem.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 5939D13C2CECB0B5000F6FAD /* ZMSAsserts.h in Headers */ = {isa = PBXBuildFile; fileRef = 5939D1112CECB0B5000F6FAD /* ZMSAsserts.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 5939D13D2CECB0B5000F6FAD /* ZMSDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 5939D1132CECB0B5000F6FAD /* ZMSDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 5939D13E2CECB0B5000F6FAD /* ZMSLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 5939D1182CECB0B5000F6FAD /* ZMSLogging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 598E86ED2BF4DD3100FC5438 /* WireSystemSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 598E86D82BF4DB3700FC5438 /* WireSystemSupport.framework */; }; - EE9AEC842BD1585500F7853F /* WireSystem.docc in Sources */ = {isa = PBXBuildFile; fileRef = EE9AEC832BD1585500F7853F /* WireSystem.docc */; }; + 59B5BC132CEFE95E00584043 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 59B5BC122CEFE95E00584043 /* WireFoundation */; }; + 59B5BE962CEFF1CD00584043 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 59B5BE952CEFF1CD00584043 /* CocoaLumberjackSwift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -75,42 +43,9 @@ 3ECC35191AD436750089FD4B /* WireSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WireSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 54A3272C1B99A3190004EB95 /* WireSystem Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "WireSystem Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 54F6FD891B31BB5A000EC9BB /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 5939CFC02CECAF3D000F6FAD /* CacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CacheTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/CacheTests.swift; sourceTree = ""; }; - 5939CFC12CECAF3D000F6FAD /* CircularArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CircularArrayTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/CircularArrayTests.swift; sourceTree = ""; }; - 5939CFC22CECAF3D000F6FAD /* DispatchGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DispatchGroupTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/DispatchGroupTests.swift; sourceTree = ""; }; - 5939CFC32CECAF3D000F6FAD /* DispatchQueueHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DispatchQueueHelperTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/DispatchQueueHelperTests.swift; sourceTree = ""; }; - 5939CFC42CECAF3D000F6FAD /* ExpiringActivityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExpiringActivityTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/ExpiringActivityTests.swift; sourceTree = ""; }; - 5939CFC52CECAF3D000F6FAD /* PopoverPresentationControllerConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PopoverPresentationControllerConfigurationTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/PopoverPresentationControllerConfigurationTests.swift; sourceTree = ""; }; - 5939CFC62CECAF3D000F6FAD /* SanitizedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SanitizedStringTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/SanitizedStringTests.swift; sourceTree = ""; }; - 5939CFC72CECAF3D000F6FAD /* Test-Bridging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Test-Bridging.h"; sourceTree = ""; }; - 5939CFC82CECAF3D000F6FAD /* TimePointTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TimePointTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/TimePointTests.swift; sourceTree = ""; }; - 5939CFC92CECAF3D000F6FAD /* ZMAssertionDumpFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZMAssertionDumpFileTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/ZMAssertionDumpFileTests.swift; sourceTree = ""; }; - 5939CFCA2CECAF3D000F6FAD /* ZMDefinesTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ZMDefinesTest.m; sourceTree = ""; }; - 5939CFCB2CECAF3D000F6FAD /* ZMLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZMLogTests.swift; path = ../../WireFoundation/Tests/WireSystemTests/ZMLogTests.swift; sourceTree = ""; }; - 5939D1042CECB0B5000F6FAD /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Cache.swift; path = ../../WireFoundation/Sources/WireSystem/Cache.swift; sourceTree = ""; }; - 5939D1052CECB0B5000F6FAD /* CircularArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CircularArray.swift; path = ../../WireFoundation/Sources/WireSystem/CircularArray.swift; sourceTree = ""; }; - 5939D1062CECB0B5000F6FAD /* DispatchQueue+ZMSDispatchGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "DispatchQueue+ZMSDispatchGroup.swift"; path = "../../WireFoundation/Sources/WireSystem/DispatchQueue+ZMSDispatchGroup.swift"; sourceTree = ""; }; - 5939D1072CECB0B5000F6FAD /* ExpiringActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExpiringActivity.swift; path = ../../WireFoundation/Sources/WireSystem/ExpiringActivity.swift; sourceTree = ""; }; - 5939D1082CECB0B5000F6FAD /* GroupQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GroupQueue.swift; path = ../../WireFoundation/Sources/WireSystem/GroupQueue.swift; sourceTree = ""; }; - 5939D1092CECB0B5000F6FAD /* PopoverPresentationControllerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PopoverPresentationControllerConfiguration.swift; path = ../../WireFoundation/Sources/WireSystem/PopoverPresentationControllerConfiguration.swift; sourceTree = ""; }; - 5939D10A2CECB0B5000F6FAD /* SafeForLoggingStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SafeForLoggingStringConvertible.swift; path = ../../WireFoundation/Sources/WireSystem/SafeForLoggingStringConvertible.swift; sourceTree = ""; }; - 5939D10B2CECB0B5000F6FAD /* SanitizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SanitizedString.swift; path = ../../WireFoundation/Sources/WireSystem/SanitizedString.swift; sourceTree = ""; }; - 5939D10C2CECB0B5000F6FAD /* TimePoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TimePoint.swift; path = ../../WireFoundation/Sources/WireSystem/TimePoint.swift; sourceTree = ""; }; - 5939D10D2CECB0B5000F6FAD /* UserDefaults+Temporary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "UserDefaults+Temporary.swift"; path = "../../WireFoundation/Sources/WireSystem/UserDefaults+Temporary.swift"; sourceTree = ""; }; - 5939D10E2CECB0B5000F6FAD /* WireSystem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WireSystem.h; sourceTree = ""; }; - 5939D10F2CECB0B5000F6FAD /* ZMAssertionDumpFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZMAssertionDumpFile.swift; path = ../../WireFoundation/Sources/WireSystem/ZMAssertionDumpFile.swift; sourceTree = ""; }; - 5939D1102CECB0B5000F6FAD /* ZMLogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZMLogLevel.swift; path = ../../WireFoundation/Sources/WireSystem/ZMLogLevel.swift; sourceTree = ""; }; - 5939D1112CECB0B5000F6FAD /* ZMSAsserts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ZMSAsserts.h; sourceTree = ""; }; - 5939D1122CECB0B5000F6FAD /* ZMSAsserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZMSAsserts.swift; path = ../../WireFoundation/Sources/WireSystem/ZMSAsserts.swift; sourceTree = ""; }; - 5939D1132CECB0B5000F6FAD /* ZMSDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ZMSDefines.h; sourceTree = ""; }; - 5939D1142CECB0B5000F6FAD /* ZMSDispatchGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZMSDispatchGroup.swift; path = ../../WireFoundation/Sources/WireSystem/ZMSDispatchGroup.swift; sourceTree = ""; }; - 5939D1152CECB0B5000F6FAD /* ZMSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZMSLog.swift; path = ../../WireFoundation/Sources/WireSystem/ZMSLog.swift; sourceTree = ""; }; - 5939D1162CECB0B5000F6FAD /* ZMSLog+Levels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "ZMSLog+Levels.swift"; path = "../../WireFoundation/Sources/WireSystem/ZMSLog+Levels.swift"; sourceTree = ""; }; - 5939D1172CECB0B5000F6FAD /* ZMSLog+Recording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "ZMSLog+Recording.swift"; path = "../../WireFoundation/Sources/WireSystem/ZMSLog+Recording.swift"; sourceTree = ""; }; - 5939D1182CECB0B5000F6FAD /* ZMSLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ZMSLogging.h; sourceTree = ""; }; 598E86D82BF4DB3700FC5438 /* WireSystemSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WireSystemSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 59B5BC112CEFE67800584043 /* WireSystem.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = WireSystem.docc; sourceTree = ""; }; E6B58E402B21D8920046B6E1 /* ZipArchive.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZipArchive.xcframework; path = ../Carthage/Build/ZipArchive.xcframework; sourceTree = ""; }; - EE9AEC832BD1585500F7853F /* WireSystem.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; name = WireSystem.docc; path = ../WireFoundation/Sources/WireSystem/WireSystem.docc; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -125,16 +60,30 @@ ); target = 598E86D72BF4DB3700FC5438 /* WireSystemSupport */; }; + 59B5BBB52CEFE5FB00584043 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + publicHeaders = ( + WireSystem.h, + ZMSAsserts.h, + ZMSDefines.h, + ZMSLogging.h, + ); + target = 3ECC350F1AD436750089FD4B /* WireSystem */; + }; + 59B5BBBA2CEFE60A00584043 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + ZMDefinesTest.m, + ); + target = 54A3272B1B99A3190004EB95 /* WireSystem Tests */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 5939CB6D2CEBB29F000F6FAD /* Resources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Resources; sourceTree = ""; }; 5939CB872CEBB2BA000F6FAD /* Support */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (5939CB8A2CEBB2BA000F6FAD /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Support; sourceTree = ""; }; - 5939CFDA2CECAF45000F6FAD /* NavigationControllerDelegate */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = NavigationControllerDelegate; path = ../../WireFoundation/Tests/WireSystemTests/NavigationControllerDelegate; sourceTree = ""; }; - 5939D1412CECB0BA000F6FAD /* DateProviding */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = DateProviding; path = ../../WireFoundation/Sources/WireSystem/DateProviding; sourceTree = ""; }; - 5939D1492CECB0BC000F6FAD /* Extensions */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = Extensions; path = ../../WireFoundation/Sources/WireSystem/Extensions; sourceTree = ""; }; - 5939D1572CECB0BE000F6FAD /* Logging */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = Logging; path = ../../WireFoundation/Sources/WireSystem/Logging; sourceTree = ""; }; - 5939D1632CECB0C0000F6FAD /* NavigationControllerDelegate */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = NavigationControllerDelegate; path = ../../WireFoundation/Sources/WireSystem/NavigationControllerDelegate; sourceTree = ""; }; + 59B5BBB02CEFE5FB00584043 /* Source */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (59B5BBB52CEFE5FB00584043 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Source; sourceTree = ""; }; + 59B5BBB82CEFE60A00584043 /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (59B5BBBA2CEFE60A00584043 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -143,7 +92,8 @@ buildActionMask = 2147483647; files = ( 591B6E892C8B0A33009F8A7B /* ZipArchive.xcframework in Frameworks */, - 013334BC2C204AB0002D97DB /* CocoaLumberjackSwift in Frameworks */, + 59B5BC132CEFE95E00584043 /* WireFoundation in Frameworks */, + 59B5BE962CEFF1CD00584043 /* CocoaLumberjackSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -170,12 +120,12 @@ 3ECC34C31AD430BB0089FD4B = { isa = PBXGroup; children = ( - EE9AEC832BD1585500F7853F /* WireSystem.docc */, + 59B5BC112CEFE67800584043 /* WireSystem.docc */, 54F6FD891B31BB5A000EC9BB /* README.md */, 5939CB6D2CEBB29F000F6FAD /* Resources */, - 5939D1192CECB0B5000F6FAD /* Source */, + 59B5BBB02CEFE5FB00584043 /* Source */, 5939CB872CEBB2BA000F6FAD /* Support */, - 5939CFCC2CECAF3D000F6FAD /* Tests */, + 59B5BBB82CEFE60A00584043 /* Tests */, 3ECC34E61AD433520089FD4B /* Products */, E66EC68A2B21D179003052F7 /* Frameworks */, ); @@ -195,58 +145,6 @@ name = Products; sourceTree = ""; }; - 5939CFCC2CECAF3D000F6FAD /* Tests */ = { - isa = PBXGroup; - children = ( - 5939CFDA2CECAF45000F6FAD /* NavigationControllerDelegate */, - 5939CFC02CECAF3D000F6FAD /* CacheTests.swift */, - 5939CFC12CECAF3D000F6FAD /* CircularArrayTests.swift */, - 5939CFC22CECAF3D000F6FAD /* DispatchGroupTests.swift */, - 5939CFC32CECAF3D000F6FAD /* DispatchQueueHelperTests.swift */, - 5939CFC42CECAF3D000F6FAD /* ExpiringActivityTests.swift */, - 5939CFC52CECAF3D000F6FAD /* PopoverPresentationControllerConfigurationTests.swift */, - 5939CFC62CECAF3D000F6FAD /* SanitizedStringTests.swift */, - 5939CFC72CECAF3D000F6FAD /* Test-Bridging.h */, - 5939CFC82CECAF3D000F6FAD /* TimePointTests.swift */, - 5939CFC92CECAF3D000F6FAD /* ZMAssertionDumpFileTests.swift */, - 5939CFCA2CECAF3D000F6FAD /* ZMDefinesTest.m */, - 5939CFCB2CECAF3D000F6FAD /* ZMLogTests.swift */, - ); - path = Tests; - sourceTree = ""; - }; - 5939D1192CECB0B5000F6FAD /* Source */ = { - isa = PBXGroup; - children = ( - 5939D1412CECB0BA000F6FAD /* DateProviding */, - 5939D1492CECB0BC000F6FAD /* Extensions */, - 5939D1572CECB0BE000F6FAD /* Logging */, - 5939D1632CECB0C0000F6FAD /* NavigationControllerDelegate */, - 5939D1042CECB0B5000F6FAD /* Cache.swift */, - 5939D1052CECB0B5000F6FAD /* CircularArray.swift */, - 5939D1062CECB0B5000F6FAD /* DispatchQueue+ZMSDispatchGroup.swift */, - 5939D1072CECB0B5000F6FAD /* ExpiringActivity.swift */, - 5939D1082CECB0B5000F6FAD /* GroupQueue.swift */, - 5939D1092CECB0B5000F6FAD /* PopoverPresentationControllerConfiguration.swift */, - 5939D10A2CECB0B5000F6FAD /* SafeForLoggingStringConvertible.swift */, - 5939D10B2CECB0B5000F6FAD /* SanitizedString.swift */, - 5939D10C2CECB0B5000F6FAD /* TimePoint.swift */, - 5939D10D2CECB0B5000F6FAD /* UserDefaults+Temporary.swift */, - 5939D10E2CECB0B5000F6FAD /* WireSystem.h */, - 5939D10F2CECB0B5000F6FAD /* ZMAssertionDumpFile.swift */, - 5939D1102CECB0B5000F6FAD /* ZMLogLevel.swift */, - 5939D1112CECB0B5000F6FAD /* ZMSAsserts.h */, - 5939D1122CECB0B5000F6FAD /* ZMSAsserts.swift */, - 5939D1132CECB0B5000F6FAD /* ZMSDefines.h */, - 5939D1142CECB0B5000F6FAD /* ZMSDispatchGroup.swift */, - 5939D1152CECB0B5000F6FAD /* ZMSLog.swift */, - 5939D1162CECB0B5000F6FAD /* ZMSLog+Levels.swift */, - 5939D1172CECB0B5000F6FAD /* ZMSLog+Recording.swift */, - 5939D1182CECB0B5000F6FAD /* ZMSLogging.h */, - ); - path = Source; - sourceTree = ""; - }; E66EC68A2B21D179003052F7 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -262,10 +160,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 5939D13B2CECB0B5000F6FAD /* WireSystem.h in Headers */, - 5939D13C2CECB0B5000F6FAD /* ZMSAsserts.h in Headers */, - 5939D13D2CECB0B5000F6FAD /* ZMSDefines.h in Headers */, - 5939D13E2CECB0B5000F6FAD /* ZMSLogging.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -294,14 +188,12 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( - 5939D1412CECB0BA000F6FAD /* DateProviding */, - 5939D1492CECB0BC000F6FAD /* Extensions */, - 5939D1572CECB0BE000F6FAD /* Logging */, - 5939D1632CECB0C0000F6FAD /* NavigationControllerDelegate */, + 59B5BBB02CEFE5FB00584043 /* Source */, ); name = WireSystem; packageProductDependencies = ( - 013334BB2C204AB0002D97DB /* CocoaLumberjackSwift */, + 59B5BC122CEFE95E00584043 /* WireFoundation */, + 59B5BE952CEFF1CD00584043 /* CocoaLumberjackSwift */, ); productName = SyncEngineSystem; productReference = 3ECC35191AD436750089FD4B /* WireSystem.framework */; @@ -321,9 +213,6 @@ 54A327341B99A3190004EB95 /* PBXTargetDependency */, 598E86F02BF4DD3100FC5438 /* PBXTargetDependency */, ); - fileSystemSynchronizedGroups = ( - 5939CFDA2CECAF45000F6FAD /* NavigationControllerDelegate */, - ); name = "WireSystem Tests"; productName = "ZMCSystem-ios Tests"; productReference = 54A3272C1B99A3190004EB95 /* WireSystem Tests.xctest */; @@ -381,7 +270,7 @@ ); mainGroup = 3ECC34C31AD430BB0089FD4B; packageReferences = ( - 013334BA2C204AB0002D97DB /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, + 59B5BE942CEFF1CD00584043 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, ); productRefGroup = 3ECC34E61AD433520089FD4B /* Products */; projectDirPath = ""; @@ -460,24 +349,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5939D12A2CECB0B5000F6FAD /* Cache.swift in Sources */, - 5939D12B2CECB0B5000F6FAD /* CircularArray.swift in Sources */, - 5939D12C2CECB0B5000F6FAD /* DispatchQueue+ZMSDispatchGroup.swift in Sources */, - 5939D12D2CECB0B5000F6FAD /* ExpiringActivity.swift in Sources */, - 5939D12E2CECB0B5000F6FAD /* GroupQueue.swift in Sources */, - 5939D12F2CECB0B5000F6FAD /* PopoverPresentationControllerConfiguration.swift in Sources */, - 5939D1302CECB0B5000F6FAD /* SafeForLoggingStringConvertible.swift in Sources */, - 5939D1312CECB0B5000F6FAD /* SanitizedString.swift in Sources */, - 5939D1322CECB0B5000F6FAD /* TimePoint.swift in Sources */, - 5939D1332CECB0B5000F6FAD /* UserDefaults+Temporary.swift in Sources */, - 5939D1342CECB0B5000F6FAD /* ZMAssertionDumpFile.swift in Sources */, - 5939D1352CECB0B5000F6FAD /* ZMLogLevel.swift in Sources */, - 5939D1362CECB0B5000F6FAD /* ZMSAsserts.swift in Sources */, - 5939D1372CECB0B5000F6FAD /* ZMSDispatchGroup.swift in Sources */, - 5939D1382CECB0B5000F6FAD /* ZMSLog.swift in Sources */, - 5939D1392CECB0B5000F6FAD /* ZMSLog+Levels.swift in Sources */, - 5939D13A2CECB0B5000F6FAD /* ZMSLog+Recording.swift in Sources */, - EE9AEC842BD1585500F7853F /* WireSystem.docc in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -485,17 +356,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5939CFCE2CECAF3D000F6FAD /* CacheTests.swift in Sources */, - 5939CFCF2CECAF3D000F6FAD /* CircularArrayTests.swift in Sources */, - 5939CFD02CECAF3D000F6FAD /* DispatchGroupTests.swift in Sources */, - 5939CFD12CECAF3D000F6FAD /* DispatchQueueHelperTests.swift in Sources */, - 5939CFD22CECAF3D000F6FAD /* ExpiringActivityTests.swift in Sources */, - 5939CFD32CECAF3D000F6FAD /* PopoverPresentationControllerConfigurationTests.swift in Sources */, - 5939CFD42CECAF3D000F6FAD /* SanitizedStringTests.swift in Sources */, - 5939CFD52CECAF3D000F6FAD /* TimePointTests.swift in Sources */, - 5939CFD62CECAF3D000F6FAD /* ZMAssertionDumpFileTests.swift in Sources */, - 5939CFD72CECAF3D000F6FAD /* ZMDefinesTest.m in Sources */, - 5939CFD82CECAF3D000F6FAD /* ZMLogTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -806,7 +666,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 013334BA2C204AB0002D97DB /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { + 59B5BE942CEFF1CD00584043 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CocoaLumberjack/CocoaLumberjack"; requirement = { @@ -817,9 +677,13 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 013334BB2C204AB0002D97DB /* CocoaLumberjackSwift */ = { + 59B5BC122CEFE95E00584043 /* WireFoundation */ = { + isa = XCSwiftPackageProductDependency; + productName = WireFoundation; + }; + 59B5BE952CEFF1CD00584043 /* CocoaLumberjackSwift */ = { isa = XCSwiftPackageProductDependency; - package = 013334BA2C204AB0002D97DB /* XCRemoteSwiftPackageReference "CocoaLumberjack" */; + package = 59B5BE942CEFF1CD00584043 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */; productName = CocoaLumberjackSwift; }; /* End XCSwiftPackageProductDependency section */ diff --git a/wire-ios/Wire-iOS/Resources/Configuration/Version.xcconfig b/wire-ios/Wire-iOS/Resources/Configuration/Version.xcconfig index 4c4d370b41c..e7c0dcb2358 100644 --- a/wire-ios/Wire-iOS/Resources/Configuration/Version.xcconfig +++ b/wire-ios/Wire-iOS/Resources/Configuration/Version.xcconfig @@ -16,4 +16,4 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -WIRE_SHORT_VERSION = 3.114.0 +WIRE_SHORT_VERSION = 3.115.0 diff --git a/wire-ios/Wire-iOS/Resources/ar.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ar.lproj/Localizable.strings index a29ef63cfbb..800ad948f5e 100644 --- a/wire-ios/Wire-iOS/Resources/ar.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ar.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "مفعل"; "general.loading" = "جارِ التحميل…"; "general.paste" = "لصق"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "في جهات الاتصال"; diff --git a/wire-ios/Wire-iOS/Resources/be.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/be.lproj/Localizable.strings index f61b38e2b52..0cc87ecda1c 100644 --- a/wire-ios/Wire-iOS/Resources/be.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/be.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/bg.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/bg.lproj/Localizable.strings index 0944cf8dba8..a2caf54a02d 100644 --- a/wire-ios/Wire-iOS/Resources/bg.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/bg.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/bn.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/bn.lproj/Localizable.strings index eaddd8fc022..312a7521747 100644 --- a/wire-ios/Wire-iOS/Resources/bn.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/bn.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/ca.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ca.lproj/Localizable.strings index a2b50af30f0..882c8a91376 100644 --- a/wire-ios/Wire-iOS/Resources/ca.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ca.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Engega"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/cs.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/cs.lproj/Localizable.strings index c2c107fdcfd..e59d9f2b31e 100644 --- a/wire-ios/Wire-iOS/Resources/cs.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/cs.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Zapnuto"; "general.loading" = "Načítání…"; "general.paste" = "Vložit"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "je v kontaktech"; diff --git a/wire-ios/Wire-iOS/Resources/da.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/da.lproj/Localizable.strings index 9016c3af33f..445b6408ed9 100644 --- a/wire-ios/Wire-iOS/Resources/da.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/da.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Aktiv"; "general.loading" = "Loading…"; "general.paste" = "Indsæt"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "i Kontaktpersoner"; diff --git a/wire-ios/Wire-iOS/Resources/de.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/de.lproj/Localizable.strings index 5cb02844ef3..fe87aa62d70 100644 --- a/wire-ios/Wire-iOS/Resources/de.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/de.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "An"; "general.loading" = "Wird geladen…"; "general.paste" = "Einfügen"; +"general.or" = "oder"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "Sie haben noch keine Kontakte. Suchen Sie nach Personen auf {{brandName}} und treten Sie in Kontakt."; "conversation_list.empty_placeholder.oneonone.button" = "Mit Personen verbinden"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "Es konnten keine Unterhaltungen gefunden werden.\n\nVerbinden Sie sich mit Personen oder erstellen Sie eine neue Gruppe:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "Keine Ergebnisse gefunden."; + +"conversation_list.empty_placeholder.search.button.phone" = "Neue Unterhaltung"; +"conversation_list.empty_placeholder.search.button.ipad" = "Beginnen Sie eine neue Unterhaltung"; +"conversation_list.empty_placeholder.search.connect_button" = "Mit Personen verbinden"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Kontakte"; diff --git a/wire-ios/Wire-iOS/Resources/el.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/el.lproj/Localizable.strings index 0473b95cd78..a1acc9965fc 100644 --- a/wire-ios/Wire-iOS/Resources/el.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/el.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Φόρτωση…"; "general.paste" = "Επικόλληση"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "στις Επαφές"; diff --git a/wire-ios/Wire-iOS/Resources/es.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/es.lproj/Localizable.strings index c36c71b1ba1..bd2bcc737fc 100644 --- a/wire-ios/Wire-iOS/Resources/es.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/es.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "En"; "general.loading" = "Cargando..."; "general.paste" = "Pegar"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "en contactos"; diff --git a/wire-ios/Wire-iOS/Resources/et.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/et.lproj/Localizable.strings index aeb0b9a10c7..5391e0a8ff1 100644 --- a/wire-ios/Wire-iOS/Resources/et.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/et.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Sees"; "general.loading" = "Laadimine…"; "general.paste" = "Kleebi"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "seadme kontaktides"; diff --git a/wire-ios/Wire-iOS/Resources/eu.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/eu.lproj/Localizable.strings index c15137945d7..9fffcfb32dd 100644 --- a/wire-ios/Wire-iOS/Resources/eu.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/eu.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "Kontaktuetan"; diff --git a/wire-ios/Wire-iOS/Resources/fa.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/fa.lproj/Localizable.strings index c1ca2ed8717..00fa2df01f3 100644 --- a/wire-ios/Wire-iOS/Resources/fa.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/fa.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "روشن"; "general.loading" = "در حال بارگیری…"; "general.paste" = "چسباندن"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "در قسمت مخاطبین"; diff --git a/wire-ios/Wire-iOS/Resources/fi.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/fi.lproj/Localizable.strings index 9374f0967e5..ebb4cfd2258 100644 --- a/wire-ios/Wire-iOS/Resources/fi.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/fi.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Käytössä"; "general.loading" = "Ladataan…"; "general.paste" = "Liitä"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "yhteystiedoissa"; diff --git a/wire-ios/Wire-iOS/Resources/fr.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/fr.lproj/Localizable.strings index 66f575edd7a..548edc6a352 100644 --- a/wire-ios/Wire-iOS/Resources/fr.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/fr.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Activé"; "general.loading" = "Chargement…"; "general.paste" = "Coller"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "dans les contacts"; diff --git a/wire-ios/Wire-iOS/Resources/he.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/he.lproj/Localizable.strings index f43f29ebe2b..1b1bb90f478 100644 --- a/wire-ios/Wire-iOS/Resources/he.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/he.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/hr.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/hr.lproj/Localizable.strings index 75e86f8733a..36036814296 100644 --- a/wire-ios/Wire-iOS/Resources/hr.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/hr.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Uključeno"; "general.loading" = "Učitavanje…"; "general.paste" = "Zalijepi"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "u kontaktima"; diff --git a/wire-ios/Wire-iOS/Resources/hu.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/hu.lproj/Localizable.strings index 0acad114ce0..b99a2979a4a 100644 --- a/wire-ios/Wire-iOS/Resources/hu.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/hu.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Be"; "general.loading" = "Betöltés…"; "general.paste" = "Beillesztés"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "Szerepel a Névjegyzékedben is"; diff --git a/wire-ios/Wire-iOS/Resources/id.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/id.lproj/Localizable.strings index 2fe9b7d418b..e435294e102 100644 --- a/wire-ios/Wire-iOS/Resources/id.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/id.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Memuat ..."; "general.paste" = "Tempel"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "di Kontak"; diff --git a/wire-ios/Wire-iOS/Resources/is.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/is.lproj/Localizable.strings index ea611b771f7..84b3ca9fe5c 100644 --- a/wire-ios/Wire-iOS/Resources/is.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/is.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Kveikt"; "general.loading" = "Hleð…"; "general.paste" = "Setja inn"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "í tengiliðum"; diff --git a/wire-ios/Wire-iOS/Resources/it.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/it.lproj/Localizable.strings index a7566e51d93..133f774f25c 100644 --- a/wire-ios/Wire-iOS/Resources/it.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/it.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Caricamento…"; "general.paste" = "Incolla"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contatti"; diff --git a/wire-ios/Wire-iOS/Resources/ja.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ja.lproj/Localizable.strings index 8a05c582dd8..483a60e477f 100644 --- a/wire-ios/Wire-iOS/Resources/ja.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ja.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "オン"; "general.loading" = "読み込み中…"; "general.paste" = "ペースト"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = "\U200B"; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "連絡先"; diff --git a/wire-ios/Wire-iOS/Resources/ka.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ka.lproj/Localizable.strings index 3c8f5abfa8b..ce983dc3910 100644 --- a/wire-ios/Wire-iOS/Resources/ka.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ka.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/ko.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ko.lproj/Localizable.strings index c72a4879a79..8b0c1ef3422 100644 --- a/wire-ios/Wire-iOS/Resources/ko.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ko.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "켜기"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "연락처에서"; diff --git a/wire-ios/Wire-iOS/Resources/lt.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/lt.lproj/Localizable.strings index ca7192e9439..29dc06efc11 100644 --- a/wire-ios/Wire-iOS/Resources/lt.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/lt.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Įjungta"; "general.loading" = "Kraunama…"; "general.paste" = "Įdėti"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "konktaktuose"; diff --git a/wire-ios/Wire-iOS/Resources/lv.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/lv.lproj/Localizable.strings index 6946c9c1238..1968b69eb66 100644 --- a/wire-ios/Wire-iOS/Resources/lv.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/lv.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Ievietot"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/ms.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ms.lproj/Localizable.strings index c18d30227c0..4fa9418a1c7 100644 --- a/wire-ios/Wire-iOS/Resources/ms.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ms.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/my.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/my.lproj/Localizable.strings index 3c8f5abfa8b..ce983dc3910 100644 --- a/wire-ios/Wire-iOS/Resources/my.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/my.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/nl.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/nl.lproj/Localizable.strings index cb66286d9d6..7ca16b7b0af 100644 --- a/wire-ios/Wire-iOS/Resources/nl.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/nl.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Aan"; "general.loading" = "Laden…"; "general.paste" = "Plakken"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacten"; diff --git a/wire-ios/Wire-iOS/Resources/no.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/no.lproj/Localizable.strings index f063bc2bcc3..4394aae7689 100644 --- a/wire-ios/Wire-iOS/Resources/no.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/no.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "På"; "general.loading" = "Laster…"; "general.paste" = "Lim inn"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "i kontakter"; diff --git a/wire-ios/Wire-iOS/Resources/pl.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/pl.lproj/Localizable.strings index 0d0f20b21a2..15ade518a7b 100644 --- a/wire-ios/Wire-iOS/Resources/pl.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/pl.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Włączone"; "general.loading" = "Wczytuje…"; "general.paste" = "Wklej"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "w kontaktach"; diff --git a/wire-ios/Wire-iOS/Resources/pt-BR.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/pt-BR.lproj/Localizable.strings index 18eae407962..84c6352376f 100644 --- a/wire-ios/Wire-iOS/Resources/pt-BR.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/pt-BR.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Ligado"; "general.loading" = "Carregando…"; "general.paste" = "Colar"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "nos Contatos"; diff --git a/wire-ios/Wire-iOS/Resources/pt.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/pt.lproj/Localizable.strings index 640e2820764..6aa399cc126 100644 --- a/wire-ios/Wire-iOS/Resources/pt.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/pt.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Ligado"; "general.loading" = "A carregar…"; "general.paste" = "Colar"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "em Contactos"; diff --git a/wire-ios/Wire-iOS/Resources/ro.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ro.lproj/Localizable.strings index 5d153d6883f..a663f8367b6 100644 --- a/wire-ios/Wire-iOS/Resources/ro.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ro.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Se încarcă…"; "general.paste" = "Lipește"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "În contacte"; diff --git a/wire-ios/Wire-iOS/Resources/ru.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ru.lproj/Localizable.strings index fc798cbf396..832d3fec1d2 100644 --- a/wire-ios/Wire-iOS/Resources/ru.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ru.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Вкл."; "general.loading" = "Загрузка…"; "general.paste" = "Вставить"; +"general.or" = "или"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -155,7 +156,14 @@ "conversation_list.empty_placeholder.favorite.link" = "Как помечать беседы как избранные"; "conversation_list.empty_placeholder.group.subheadline" = "Вы еще не участвуете ни в одной групповой беседе. Начните новую!"; "conversation_list.empty_placeholder.oneonone.subheadline" = "У вас пока нет контактов. Ищите друзей в %@ и общайтесь."; -"conversation_list.empty_placeholder.oneonone.button" = "Связаться с друзьями"; +"conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; + +"conversation_list.empty_placeholder.search.subheadline.phone" = "Не удалось найти ни одной беседы.\n\nСвяжитесь с пользователями или начните новую групповую беседу:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "Ничего не найдено."; + +"conversation_list.empty_placeholder.search.button.phone" = "Новая беседа"; +"conversation_list.empty_placeholder.search.button.ipad" = "Начать новую беседу"; +"conversation_list.empty_placeholder.search.connect_button" = "Связаться с пользователями"; // Profile Header View "conversation.connection_view.in_address_book" = "в контактах"; diff --git a/wire-ios/Wire-iOS/Resources/si.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/si.lproj/Localizable.strings index a86842d4876..633148e41d2 100644 --- a/wire-ios/Wire-iOS/Resources/si.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/si.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "සක්‍රියයි"; "general.loading" = "පූරණය වෙමින්…"; "general.paste" = "අලවන්න"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/sk.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/sk.lproj/Localizable.strings index 0b6d690b564..1ee680b3e2f 100644 --- a/wire-ios/Wire-iOS/Resources/sk.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/sk.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Zapnúť"; "general.loading" = "Načítavanie…"; "general.paste" = "Prilepiť"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "V kontaktoch"; diff --git a/wire-ios/Wire-iOS/Resources/sl.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/sl.lproj/Localizable.strings index a30f45c25b4..b3a87f77b3a 100644 --- a/wire-ios/Wire-iOS/Resources/sl.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/sl.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Prilepi"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "v stikih"; diff --git a/wire-ios/Wire-iOS/Resources/sr-Latn.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/sr-Latn.lproj/Localizable.strings index 699c8efbc26..0bf8a197d73 100644 --- a/wire-ios/Wire-iOS/Resources/sr-Latn.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/sr-Latn.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/sr.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/sr.lproj/Localizable.strings index 27a046ce243..bc5b8689031 100644 --- a/wire-ios/Wire-iOS/Resources/sr.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/sr.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "укључено"; "general.loading" = "Loading…"; "general.paste" = "Налепи"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "у контактима"; diff --git a/wire-ios/Wire-iOS/Resources/sv.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/sv.lproj/Localizable.strings index 89442aa32ad..0df5f4c3f91 100644 --- a/wire-ios/Wire-iOS/Resources/sv.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/sv.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "På"; "general.loading" = "Laddar…"; "general.paste" = "Klistra in"; +"general.or" = "eller"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "Inga resultat hittades."; + +"conversation_list.empty_placeholder.search.button.phone" = "Ny konversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Starta en ny konversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "i kontakter"; diff --git a/wire-ios/Wire-iOS/Resources/th.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/th.lproj/Localizable.strings index 3c8f5abfa8b..ce983dc3910 100644 --- a/wire-ios/Wire-iOS/Resources/th.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/th.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/tr.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/tr.lproj/Localizable.strings index 79a6e8d4443..466ca465d0a 100644 --- a/wire-ios/Wire-iOS/Resources/tr.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/tr.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Açık"; "general.loading" = "Yükleniyor…"; "general.paste" = "Yapıştır"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "Kişilerde"; diff --git a/wire-ios/Wire-iOS/Resources/uk.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/uk.lproj/Localizable.strings index bf4df8d4067..7909566cd90 100644 --- a/wire-ios/Wire-iOS/Resources/uk.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/uk.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Увімк."; "general.loading" = "Завантаження…"; "general.paste" = "Вставити"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "в Контактах"; diff --git a/wire-ios/Wire-iOS/Resources/ur.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/ur.lproj/Localizable.strings index 3c8f5abfa8b..ce983dc3910 100644 --- a/wire-ios/Wire-iOS/Resources/ur.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/ur.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "On"; "general.loading" = "Loading…"; "general.paste" = "Paste"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "in Contacts"; diff --git a/wire-ios/Wire-iOS/Resources/vi.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/vi.lproj/Localizable.strings index 99e5701b979..72368dde498 100644 --- a/wire-ios/Wire-iOS/Resources/vi.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/vi.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "Bật"; "general.loading" = "Đang tải…"; "general.paste" = "Dán"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = " "; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "trong Danh bạ"; diff --git a/wire-ios/Wire-iOS/Resources/zh-Hans.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/zh-Hans.lproj/Localizable.strings index 1785770e3b4..291c4569af9 100644 --- a/wire-ios/Wire-iOS/Resources/zh-Hans.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/zh-Hans.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "开启"; "general.loading" = "载入中..."; "general.paste" = "粘贴"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = "\U200B"; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "在联系人中"; diff --git a/wire-ios/Wire-iOS/Resources/zh-Hant.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/zh-Hant.lproj/Localizable.strings index b4061910d42..7a36d699687 100644 --- a/wire-ios/Wire-iOS/Resources/zh-Hant.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/zh-Hant.lproj/Localizable.strings @@ -37,6 +37,7 @@ "general.on" = "開啟"; "general.loading" = "載入中..."; "general.paste" = "貼上"; +"general.or" = "or"; // Language like Chinese does not use space to sperate words or sentences. "general.space_between_words" = "\U200B"; @@ -157,6 +158,13 @@ "conversation_list.empty_placeholder.oneonone.subheadline" = "You have no contacts yet.\nSearch for people on %@ and get connected."; "conversation_list.empty_placeholder.oneonone.button" = "Connect with People"; +"conversation_list.empty_placeholder.search.subheadline.phone" = "No conversations could be found.\n\nConnect with people or start a new group conversation:"; +"conversation_list.empty_placeholder.search.subheadline.ipad" = "No results found."; + +"conversation_list.empty_placeholder.search.button.phone" = "New Conversation"; +"conversation_list.empty_placeholder.search.button.ipad" = "Start a new Conversation"; +"conversation_list.empty_placeholder.search.connect_button" = "Connect with People"; + // Profile Header View "conversation.connection_view.in_address_book" = "在聯絡人中"; diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift index 6ffc850bb4b..43196283395 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift @@ -445,7 +445,10 @@ extension ConversationListViewController: ConversationListContainerViewModelDele guard let self, let mainCoordinator else { return } Task { @MainActor [folderPickerViewControllerBuilder] in - let viewController = folderPickerViewControllerBuilder.build(mainCoordinator: mainCoordinator) + let viewController = folderPickerViewControllerBuilder.build( + mainCoordinator: mainCoordinator, + showCloseButton: true + ) if let sheet = viewController.sheetPresentationController { sheet.detents = [.medium(), .large()] sheet.prefersGrabberVisible = true diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift index 94879e365f7..1174256f119 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift @@ -30,7 +30,7 @@ struct FolderPickerViewControllerBuilder { } @MainActor - func build(mainCoordinator: AnyMainCoordinator) -> UIViewController { + func build(mainCoordinator: AnyMainCoordinator, showCloseButton: Bool) -> UIViewController { let folders: [FolderPickerOption] = conversationDirectory.allFolders.compactMap { guard let id = $0.remoteIdentifier, let title = $0.name else { return nil } @@ -54,7 +54,7 @@ struct FolderPickerViewControllerBuilder { let navigationStack = NavigationStack { FolderPicker( - showCloseButton: false, + showCloseButton: showCloseButton, options: folders, helpLink: WireURLs.shared.howToAddConversationToCustomFolder, selected: selected diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/SidebarViewControllerDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/SidebarViewControllerDelegate.swift index f2da52bdebb..511e1de0c68 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/SidebarViewControllerDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/SidebarViewControllerDelegate.swift @@ -51,7 +51,10 @@ final class SidebarViewControllerDelegate: WireSidebarUI.SidebarViewControllerDe @MainActor func sidebarViewController(_ viewController: SidebarViewController, didTapFoldersMenuItem frame: CGRect) { Task { - let folderPicker = folderPickerViewControllerBuilder.build(mainCoordinator: mainCoordinator) + let folderPicker = folderPickerViewControllerBuilder.build( + mainCoordinator: mainCoordinator, + showCloseButton: false + ) folderPicker.modalPresentationStyle = .popover if let popover = folderPicker.popoverPresentationController,