mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-16 06:28:27 +02:00
Add chat example
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -136,14 +136,14 @@
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</stackView>
|
||||
<button opaque="NO" alpha="0.0" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ih9-EU-BOU" userLabel="scroll Button">
|
||||
<rect key="frame" x="327" y="570" width="32" height="32"/>
|
||||
<rect key="frame" x="321" y="564" width="38" height="38"/>
|
||||
<state key="normal" image="scrolldown"/>
|
||||
<connections>
|
||||
<action selector="scrollToBottomAction:" destination="-1" eventType="touchUpInside" id="TOf-aY-J6a"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" alpha="0.0" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QHs-rM-UU8" userLabel="scroll badge" customClass="BadgeLabel" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="334.5" y="562" width="17.5" height="16.5"/>
|
||||
<rect key="frame" x="336.5" y="557.5" width="7.5" height="13.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="11"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -212,13 +212,13 @@
|
||||
</objects>
|
||||
<designables>
|
||||
<designable name="QHs-rM-UU8">
|
||||
<size key="intrinsicContentSize" width="17.5" height="16.5"/>
|
||||
<size key="intrinsicContentSize" width="7.5" height="13.5"/>
|
||||
</designable>
|
||||
</designables>
|
||||
<resources>
|
||||
<image name="new_close" width="16" height="16"/>
|
||||
<image name="room_scroll_up" width="24" height="24"/>
|
||||
<image name="scrolldown" width="32" height="32"/>
|
||||
<image name="scrolldown" width="38" height="38"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
|
||||
@@ -48,11 +48,18 @@ extension MockScreenState {
|
||||
static func screenGroup(
|
||||
themeId: ThemeIdentifier = .light,
|
||||
locale: Locale = Locale.current,
|
||||
sizeCategory: ContentSizeCategory = ContentSizeCategory.medium
|
||||
sizeCategory: ContentSizeCategory = ContentSizeCategory.medium,
|
||||
addNavigation: Bool = false
|
||||
) -> some View {
|
||||
Group {
|
||||
ForEach(0..<screensViews.count) { index in
|
||||
screensViews[index]
|
||||
if addNavigation {
|
||||
NavigationView{
|
||||
screensViews[index]
|
||||
}
|
||||
} else {
|
||||
screensViews[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
.theme(themeId)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
final class TemplateRoomChatCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: TemplateRoomChatCoordinatorParameters
|
||||
private let templateRoomChatHostingController: UIViewController
|
||||
private var templateRoomChatViewModel: TemplateRoomChatViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: (() -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: TemplateRoomChatCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
let viewModel = TemplateRoomChatViewModel(templateRoomChatService: TemplateRoomChatService(session: parameters.session))
|
||||
let view = TemplateRoomChat(viewModel: viewModel)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
||||
templateRoomChatViewModel = viewModel
|
||||
templateRoomChatHostingController = VectorHostingController(rootView: view)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func start() {
|
||||
templateRoomChatViewModel.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .cancel, .done:
|
||||
self.completion?()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.templateRoomChatHostingController
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateRoomChatCoordinatorParameters {
|
||||
let session: MXSession
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateRoomChatMessage {
|
||||
let id: String
|
||||
let body: String
|
||||
let sender: TemplateRoomChatMember
|
||||
}
|
||||
|
||||
extension TemplateRoomChatMessage: Identifiable {}
|
||||
|
||||
struct TemplateRoomChatMember {
|
||||
let id: String
|
||||
let avatarUrl: String?
|
||||
let displayName: String?
|
||||
}
|
||||
|
||||
extension TemplateRoomChatMember: Avatarable {
|
||||
var mxContentUri: String? {
|
||||
avatarUrl
|
||||
}
|
||||
|
||||
var matrixItemId: String {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
extension TemplateRoomChatMember: Identifiable {}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TemplateRoomChatStateAction {
|
||||
case updateBubbles([TemplateRoomChatBubble])
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TemplateRoomChatViewAction {
|
||||
case cancel
|
||||
case done
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateRoomChatViewModelInput {
|
||||
let messageInput: String
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TemplateRoomChatViewModelResult {
|
||||
case cancel
|
||||
case done
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateRoomChatViewState {
|
||||
var bubbles: [TemplateRoomChatBubble]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomChatService: TemplateRoomChatServiceProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let room: MXRoom
|
||||
private let eventFormatter: EventFormatter
|
||||
private var listenerReference: Any?
|
||||
|
||||
// MARK: Public
|
||||
private(set) var chatMessagesSubject: CurrentValueSubject<[TemplateRoomChatMessage], Never>
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(room: MXRoom) {
|
||||
self.room = room
|
||||
self.eventFormatter = EventFormatter(matrixSession: room.mxSession)
|
||||
let batch = room.enumeratorForStoredMessages.nextEventsBatch(50)
|
||||
let messageBatch = chatMessages(from: batch ?? [])
|
||||
chatMessagesSubject = CurrentValueSubject(messageBatch)
|
||||
}
|
||||
|
||||
|
||||
func senderForMessage(event: MXEvent) -> TemplateRoomChatMember? {
|
||||
guard let sender = event.sender else {
|
||||
return nil
|
||||
}
|
||||
let displayName = eventFormatter.senderDisplayName(for: event, with: room.dangerousSyncState)
|
||||
let avatarUrl = eventFormatter.senderAvatarUrl(for: event, with: room.dangerousSyncState)
|
||||
return TemplateRoomChatMember(id: sender, avatarUrl: avatarUrl, displayName: displayName)
|
||||
}
|
||||
|
||||
private func chatMessages(from events: [MXEvent]) -> [TemplateRoomChatMessage] {
|
||||
|
||||
eve
|
||||
return events
|
||||
.filter({ event in
|
||||
event.type == kMXEventTypeStringRoomMessage
|
||||
&& event.content["msgtype"] as? String == kMXMessageTypeText
|
||||
})
|
||||
.compactMap({ event -> TemplateRoomChatMessage? in
|
||||
guard let eventId = event.eventId,
|
||||
let eventBody = event.content["body"] as? String,
|
||||
let sender = senderForMessage(event: event)
|
||||
else { return nil }
|
||||
return TemplateRoomChatMessage(id: eventId,
|
||||
body: eventBody,
|
||||
sender: sender)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
// guard let reference = listenerReference else { return }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/// Using an enum for the screen allows you define the different state cases with
|
||||
/// the relevant associated data for each case.
|
||||
@available(iOS 14.0, *)
|
||||
enum MockTemplateRoomChatScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case noRooms
|
||||
case rooms
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
TemplateRoomChat.self
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: AnyView {
|
||||
let service: MockTemplateRoomChatService
|
||||
switch self {
|
||||
case .noRooms:
|
||||
service = MockTemplateRoomChatService(messages: [])
|
||||
case .rooms:
|
||||
service = MockTemplateRoomChatService()
|
||||
}
|
||||
let viewModel = TemplateRoomChatViewModel(templateRoomChatService: service)
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return AnyView(TemplateRoomChat(viewModel: viewModel)
|
||||
.addDependency(MockAvatarService.example))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MockTemplateRoomChatService: TemplateRoomChatServiceProtocol {
|
||||
|
||||
static let mockMessages = [
|
||||
TemplateRoomChatMessage(id: "!aaabaa:matrix.org", body: "Shall I put it live?", sender: "@alice:matrix.org"),
|
||||
TemplateRoomChatMessage(id: "!bbbabb:matrix.org", body: "Yea go for it! ...and then let's head to the pub", sender: "@patrice:matrix.org"),
|
||||
TemplateRoomChatMessage(id: "!aaabaa:matrix.org", body: "Deal.", sender: "@alice:matrix.org"),
|
||||
TemplateRoomChatMessage(id: "!aaabaa:matrix.org", body: "Ok, Done. 🍻", sender: "@alice:matrix.org"),
|
||||
]
|
||||
var chatMessagesSubject: CurrentValueSubject<[TemplateRoomChatMessage], Never>
|
||||
|
||||
init(messages: [TemplateRoomChatMessage] = mockMessages) {
|
||||
chatMessagesSubject = CurrentValueSubject(messages)
|
||||
}
|
||||
|
||||
func simulateUpdate(messages: [TemplateRoomChatMessage]) {
|
||||
self.chatMessagesSubject.send(messages)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol TemplateRoomChatServiceProtocol {
|
||||
var chatMessagesSubject: CurrentValueSubject<[TemplateRoomChatMessage], Never> { get }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomChatUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockTemplateRoomChatScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return TemplateRoomChatUITests(selector: #selector(verifyTemplateRoomChatScreen))
|
||||
}
|
||||
|
||||
func verifyTemplateRoomChatScreen() throws {
|
||||
guard let screenState = screenState as? MockTemplateRoomChatScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .presence(let presence):
|
||||
verifyTemplateRoomChatPresence(presence: presence)
|
||||
case .longDisplayName(let name):
|
||||
verifyTemplateRoomChatLongName(name: name)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyTemplateRoomChatPresence(presence: TemplateRoomChatPresence) {
|
||||
let presenceText = app.staticTexts["presenceText"]
|
||||
XCTAssert(presenceText.exists)
|
||||
XCTAssert(presenceText.label == presence.title)
|
||||
}
|
||||
|
||||
func verifyTemplateRoomChatLongName(name: String) {
|
||||
let displayNameText = app.staticTexts["displayNameText"]
|
||||
XCTAssert(displayNameText.exists)
|
||||
XCTAssert(displayNameText.label == name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import Combine
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomChatViewModelTests: XCTestCase {
|
||||
private enum Constants {
|
||||
static let presenceInitialValue: TemplateRoomChatPresence = .offline
|
||||
static let displayName = "Alice"
|
||||
}
|
||||
var service: MockTemplateRoomChatService!
|
||||
var viewModel: TemplateRoomChatViewModel!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
override func setUpWithError() throws {
|
||||
service = MockTemplateRoomChatService(displayName: Constants.displayName, presence: Constants.presenceInitialValue)
|
||||
viewModel = TemplateRoomChatViewModel(templateRoomChatService: service)
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertEqual(viewModel.viewState.displayName, Constants.displayName)
|
||||
XCTAssertEqual(viewModel.viewState.presence, Constants.presenceInitialValue)
|
||||
}
|
||||
|
||||
func testFirstPresenceReceived() throws {
|
||||
let presencePublisher = viewModel.$viewState.map(\.presence).removeDuplicates().collect(1).first()
|
||||
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue])
|
||||
}
|
||||
|
||||
func testPresenceUpdatesReceived() throws {
|
||||
let presencePublisher = viewModel.$viewState.map(\.presence).removeDuplicates().collect(3).first()
|
||||
let newPresenceValue1: TemplateRoomChatPresence = .online
|
||||
let newPresenceValue2: TemplateRoomChatPresence = .idle
|
||||
service.simulateUpdate(presence: newPresenceValue1)
|
||||
service.simulateUpdate(presence: newPresenceValue2)
|
||||
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomChat: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: TemplateRoomChatViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
LazyVStack {
|
||||
|
||||
}.frame(maxHeight: .infinity)
|
||||
|
||||
HStack {
|
||||
TextField(VectorL10n.roomMessageShortPlaceholder, text: $viewModel.input.messageInput)
|
||||
.textFieldStyle(BorderedInputFieldStyle())
|
||||
Button(action: {
|
||||
|
||||
}, label: {
|
||||
Image(uiImage: Asset.Images.sendIcon.image)
|
||||
})
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
}
|
||||
.navigationTitle("Chat")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(VectorL10n.done) {
|
||||
viewModel.process(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(VectorL10n.cancel) {
|
||||
viewModel.process(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomChat_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MockTemplateRoomChatScreenState.screenGroup(addNavigation: true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomChatBubbleImage: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
let imageItem: TemplateRoomChatBubbleImageItem
|
||||
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomChatBubbleImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomChatBubbleView: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
let bubble: TemplateRoomChatBubble
|
||||
|
||||
var body: some View {
|
||||
HStack{
|
||||
AvatarImage(avatarData: bubble.avatar, size: .xSmall)
|
||||
VStack{
|
||||
Text(bubble.displayName ?? "")
|
||||
ForEach(bubble.items) { item in
|
||||
TemplateRoomChatBubbleItemView(bubbleItem: item)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
//add to a style
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomChatBubbleView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomChatViewModel: ObservableObject, TemplateRoomChatViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
private let templateRoomChatService: TemplateRoomChatServiceProtocol
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
@Published var input: TemplateRoomChatViewModelInput
|
||||
@Published private(set) var viewState: TemplateRoomChatViewState
|
||||
|
||||
var completion: ((TemplateRoomChatViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
init(templateRoomChatService: TemplateRoomChatServiceProtocol, initialState: TemplateRoomChatViewState? = nil) {
|
||||
self.input = TemplateRoomChatViewModelInput(messageInput: "")
|
||||
self.templateRoomChatService = templateRoomChatService
|
||||
self.viewState = initialState ?? Self.defaultState(templateRoomChatService: templateRoomChatService)
|
||||
|
||||
templateRoomChatService.chatMessagesSubject
|
||||
.map(Self.makeBubbles(messages:))
|
||||
.map(TemplateRoomChatStateAction.updateBubbles)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] action in
|
||||
self?.dispatch(action:action)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private static func defaultState(templateRoomChatService: TemplateRoomChatServiceProtocol) -> TemplateRoomChatViewState {
|
||||
let bubbles = makeBubbles(messages: templateRoomChatService.chatMessagesSubject.value)
|
||||
return TemplateRoomChatViewState(bubbles: bubbles)
|
||||
}
|
||||
|
||||
private static func makeBubbles(messages: [TemplateRoomChatMessage]) -> [TemplateRoomChatBubble] {
|
||||
|
||||
|
||||
messages.enumerated().forEach { i, message in
|
||||
let currentMessage = messages[i]
|
||||
if i > 0 {
|
||||
let lastMessage = messages[i-1]
|
||||
} else {
|
||||
TemplateRoomChatBubble(
|
||||
id: message.,
|
||||
avatar: <#T##AvatarInputProtocol#>,
|
||||
displayName: <#T##String?#>,
|
||||
items: <#T##[TemplateRoomChatBubbleItem]#>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func process(viewAction: TemplateRoomChatViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
cancel()
|
||||
case .done:
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
/**
|
||||
Send state actions to mutate the state.
|
||||
*/
|
||||
private func dispatch(action: TemplateRoomChatStateAction) {
|
||||
Self.reducer(state: &self.viewState, action: action)
|
||||
}
|
||||
|
||||
/**
|
||||
A redux style reducer, all modifications to state happen here. Receives a state and a state action and produces a new state.
|
||||
*/
|
||||
private static func reducer(state: inout TemplateRoomChatViewState, action: TemplateRoomChatStateAction) {
|
||||
switch action {
|
||||
case .updateBubbles(let bubbles):
|
||||
state.bubbles = bubbles
|
||||
}
|
||||
UILog.debug("[TemplateRoomChatViewModel] reducer with action \(action) produced state: \(state)")
|
||||
}
|
||||
|
||||
private func done() {
|
||||
completion?(.done)
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
completion?(.cancel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol TemplateRoomChatViewModelProtocol {
|
||||
var completion: ((TemplateRoomChatViewModelResult) -> Void)? { get set }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
final class TemplateRoomListCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: TemplateRoomListCoordinatorParameters
|
||||
private let templateRoomListHostingController: UIViewController
|
||||
private var templateRoomListViewModel: TemplateRoomListViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: (() -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: TemplateRoomListCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
let viewModel = TemplateRoomListViewModel(templateRoomListService: TemplateRoomListService(session: parameters.session))
|
||||
let view = TemplateRoomList(viewModel: viewModel)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
||||
templateRoomListViewModel = viewModel
|
||||
templateRoomListHostingController = VectorHostingController(rootView: view)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func start() {
|
||||
templateRoomListViewModel.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .cancel, .done:
|
||||
self.completion?()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.templateRoomListHostingController
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateRoomListCoordinatorParameters {
|
||||
let session: MXSession
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateRoomListRoom {
|
||||
let id: String
|
||||
let avatar: AvatarInputProtocol
|
||||
let displayName: String?
|
||||
}
|
||||
|
||||
extension TemplateRoomListRoom: Identifiable {}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TemplateRoomListStateAction {
|
||||
case updateRooms([TemplateRoomListRoom])
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TemplateRoomListViewAction {
|
||||
case cancel
|
||||
case done
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TemplateRoomListViewModelResult {
|
||||
case cancel
|
||||
case done
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateRoomListViewState {
|
||||
var rooms: [TemplateRoomListRoom]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomListService: TemplateRoomListServiceProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let session: MXSession
|
||||
private var listenerReference: Any?
|
||||
|
||||
// MARK: Public
|
||||
private(set) var roomsSubject: CurrentValueSubject<[TemplateRoomListRoom], Never>
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(session: MXSession) {
|
||||
self.session = session
|
||||
self.roomsSubject = CurrentValueSubject(session.rooms.map(TemplateRoomListRoom.init(mxRoom:)))
|
||||
// self.listenerReference = setupPresenceListener()
|
||||
}
|
||||
|
||||
deinit {
|
||||
// guard let reference = listenerReference else { return }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension TemplateRoomListRoom {
|
||||
|
||||
init(mxRoom: MXRoom) {
|
||||
TemplateRoomListRoom(id: mxRoom.roomId, avatar: mxRoom.summary.avatar, displayName: mxRoom.summary.displayname)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/// Using an enum for the screen allows you define the different state cases with
|
||||
/// the relevant associated data for each case.
|
||||
@available(iOS 14.0, *)
|
||||
enum MockTemplateRoomListScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case noRooms
|
||||
case rooms
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
TemplateRoomList.self
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: AnyView {
|
||||
let service: MockTemplateRoomListService
|
||||
switch self {
|
||||
case .noRooms:
|
||||
service = MockTemplateRoomListService(rooms: [])
|
||||
case .rooms:
|
||||
service = MockTemplateRoomListService()
|
||||
}
|
||||
let viewModel = TemplateRoomListViewModel(templateRoomListService: service)
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return AnyView(TemplateRoomList(viewModel: viewModel)
|
||||
.addDependency(MockAvatarService.example))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MockTemplateRoomListService: TemplateRoomListServiceProtocol {
|
||||
|
||||
static let mockRooms = [
|
||||
TemplateRoomListRoom(id: "!aaabaa:matrix.org", avatar: MockAvatarInput.example, displayName: "Matrix Discussion"),
|
||||
TemplateRoomListRoom(id: "!zzasds:matrix.org", avatar: MockAvatarInput.example, displayName: "Element Mobile"),
|
||||
TemplateRoomListRoom(id: "!scthve:matrix.org", avatar: MockAvatarInput.example, displayName: "Alice Personal")
|
||||
]
|
||||
var roomsSubject: CurrentValueSubject<[TemplateRoomListRoom], Never>
|
||||
|
||||
init(rooms: [TemplateRoomListRoom] = mockRooms) {
|
||||
roomsSubject = CurrentValueSubject(rooms)
|
||||
}
|
||||
|
||||
func simulateUpdate(rooms: [TemplateRoomListRoom]) {
|
||||
self.roomsSubject.send(rooms)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol TemplateRoomListServiceProtocol {
|
||||
var roomsSubject: CurrentValueSubject<[TemplateRoomListRoom], Never> { get }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomListUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockTemplateRoomListScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return TemplateRoomListUITests(selector: #selector(verifyTemplateRoomListScreen))
|
||||
}
|
||||
|
||||
func verifyTemplateRoomListScreen() throws {
|
||||
guard let screenState = screenState as? MockTemplateRoomListScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .presence(let presence):
|
||||
verifyTemplateRoomListPresence(presence: presence)
|
||||
case .longDisplayName(let name):
|
||||
verifyTemplateRoomListLongName(name: name)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyTemplateRoomListPresence(presence: TemplateRoomListPresence) {
|
||||
let presenceText = app.staticTexts["presenceText"]
|
||||
XCTAssert(presenceText.exists)
|
||||
XCTAssert(presenceText.label == presence.title)
|
||||
}
|
||||
|
||||
func verifyTemplateRoomListLongName(name: String) {
|
||||
let displayNameText = app.staticTexts["displayNameText"]
|
||||
XCTAssert(displayNameText.exists)
|
||||
XCTAssert(displayNameText.label == name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import Combine
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomListViewModelTests: XCTestCase {
|
||||
private enum Constants {
|
||||
static let presenceInitialValue: TemplateRoomListPresence = .offline
|
||||
static let displayName = "Alice"
|
||||
}
|
||||
var service: MockTemplateRoomListService!
|
||||
var viewModel: TemplateRoomListViewModel!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
override func setUpWithError() throws {
|
||||
service = MockTemplateRoomListService(displayName: Constants.displayName, presence: Constants.presenceInitialValue)
|
||||
viewModel = TemplateRoomListViewModel(templateRoomListService: service)
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertEqual(viewModel.viewState.displayName, Constants.displayName)
|
||||
XCTAssertEqual(viewModel.viewState.presence, Constants.presenceInitialValue)
|
||||
}
|
||||
|
||||
func testFirstPresenceReceived() throws {
|
||||
let presencePublisher = viewModel.$viewState.map(\.presence).removeDuplicates().collect(1).first()
|
||||
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue])
|
||||
}
|
||||
|
||||
func testPresenceUpdatesReceived() throws {
|
||||
let presencePublisher = viewModel.$viewState.map(\.presence).removeDuplicates().collect(3).first()
|
||||
let newPresenceValue1: TemplateRoomListPresence = .online
|
||||
let newPresenceValue2: TemplateRoomListPresence = .idle
|
||||
service.simulateUpdate(presence: newPresenceValue1)
|
||||
service.simulateUpdate(presence: newPresenceValue2)
|
||||
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomList: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: TemplateRoomListViewModel
|
||||
|
||||
var body: some View {
|
||||
listContent
|
||||
.navigationTitle("Rooms")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(VectorL10n.done) {
|
||||
viewModel.process(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(VectorL10n.cancel) {
|
||||
viewModel.process(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var listContent: some View {
|
||||
if viewModel.viewState.rooms.isEmpty {
|
||||
Text("No Rooms")
|
||||
} else {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(viewModel.viewState.rooms) { room in
|
||||
TemplateRoomListRow(avatar: room.avatar, displayName: room.displayName)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomList_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MockTemplateRoomListScreenState.screenGroup(addNavigation: true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomListRow: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
let avatar: AvatarInputProtocol
|
||||
let displayName: String?
|
||||
|
||||
var body: some View {
|
||||
HStack{
|
||||
AvatarImage(avatarData: avatar, size: .medium)
|
||||
Text(displayName ?? "")
|
||||
Spacer()
|
||||
}
|
||||
//add to a style
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TemplateRoomListRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice")
|
||||
.addDependency(MockAvatarService.example)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomListViewModel: ObservableObject, TemplateRoomListViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
private let templateRoomListService: TemplateRoomListServiceProtocol
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
@Published private(set) var viewState: TemplateRoomListViewState
|
||||
|
||||
var completion: ((TemplateRoomListViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
init(templateRoomListService: TemplateRoomListServiceProtocol, initialState: TemplateRoomListViewState? = nil) {
|
||||
self.templateRoomListService = templateRoomListService
|
||||
self.viewState = initialState ?? Self.defaultState(templateRoomListService: templateRoomListService)
|
||||
|
||||
templateRoomListService.roomsSubject
|
||||
.map(TemplateRoomListStateAction.updateRooms)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] action in
|
||||
self?.dispatch(action:action)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private static func defaultState(templateRoomListService: TemplateRoomListServiceProtocol) -> TemplateRoomListViewState {
|
||||
return TemplateRoomListViewState(rooms: templateRoomListService.roomsSubject.value)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func process(viewAction: TemplateRoomListViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
cancel()
|
||||
case .done:
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
/**
|
||||
Send state actions to mutate the state.
|
||||
*/
|
||||
private func dispatch(action: TemplateRoomListStateAction) {
|
||||
Self.reducer(state: &self.viewState, action: action)
|
||||
}
|
||||
|
||||
/**
|
||||
A redux style reducer, all modifications to state happen here. Receives a state and a state action and produces a new state.
|
||||
*/
|
||||
private static func reducer(state: inout TemplateRoomListViewState, action: TemplateRoomListStateAction) {
|
||||
switch action {
|
||||
case .updateRooms(let rooms):
|
||||
state.rooms = rooms
|
||||
}
|
||||
UILog.debug("[TemplateRoomListViewModel] reducer with action \(action) produced state: \(state)")
|
||||
}
|
||||
|
||||
private func done() {
|
||||
completion?(.done)
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
completion?(.cancel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol TemplateRoomListViewModelProtocol {
|
||||
var completion: ((TemplateRoomListViewModelResult) -> Void)? { get set }
|
||||
}
|
||||
34
Tools/Templates/createSwiftUITwoScreen.sh
Executable file
34
Tools/Templates/createSwiftUITwoScreen.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! $# -eq 2 ]; then
|
||||
echo "Usage: ./createSwiftUITwoScreen.sh Folder MyScreenName"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODULE_DIR="../../RiotSwiftUI/Modules"
|
||||
OUTPUT_DIR=$MODULE_DIR/$1
|
||||
SCREEN_NAME=$2
|
||||
SCREEN_VAR_NAME=`echo $SCREEN_NAME | awk '{ print tolower(substr($0, 1, 1)) substr($0, 2) }'`
|
||||
TEMPLATE_DIR=$MODULE_DIR/Template/TemplateAdvancedRoomsExample/TemplateRoomList/
|
||||
if [ -e $OUTPUT_DIR ]; then
|
||||
echo "Error: Folder ${OUTPUT_DIR} already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Create folder ${OUTPUT_DIR}"
|
||||
|
||||
mkdir -p $OUTPUT_DIR
|
||||
cp -R $TEMPLATE_DIR $OUTPUT_DIR/
|
||||
|
||||
cd $OUTPUT_DIR
|
||||
for file in $(find * -type f -print)
|
||||
do
|
||||
echo "Building ${file/TemplateRoomList/$SCREEN_NAME}..."
|
||||
perl -p -i -e "s/TemplateRoomList/"$SCREEN_NAME"/g" $file
|
||||
perl -p -i -e "s/templateRoomList/"$SCREEN_VAR_NAME"/g" $file
|
||||
|
||||
# echo "// $ createScreen.sh $@" | cat - ${file} > /tmp/$$ && mv /tmp/$$ ${file}
|
||||
# echo '// File created from TemplateAdvancedRoomsExample' | cat - ${file} > /tmp/$$ && mv /tmp/$$ ${file}
|
||||
|
||||
mv ${file} ${file/TemplateRoomList/$SCREEN_NAME}
|
||||
done
|
||||
Reference in New Issue
Block a user