Add chat example

This commit is contained in:
David Langley
2021-09-12 14:57:45 +01:00
parent a20b50028e
commit b51dbb1b63
39 changed files with 1589 additions and 8 deletions

View File

@@ -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>

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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 {}

View File

@@ -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])
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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]
}

View File

@@ -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 }
}
}

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -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 }
}

View File

@@ -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)
}
}

View File

@@ -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])
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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 }
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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 {}

View File

@@ -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])
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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]
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -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 }
}

View File

@@ -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)
}
}

View File

@@ -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])
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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 }
}

View 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