diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/Contents.json
new file mode 100644
index 000000000..48ffc5e34
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_record.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/voice_broadcast_record.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/voice_broadcast_record.svg
new file mode 100644
index 000000000..4ca9bd42c
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/voice_broadcast_record.svg
@@ -0,0 +1,6 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/Contents.json
new file mode 100644
index 000000000..157748565
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_record_pause.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/voice_broadcast_record_pause.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/voice_broadcast_record_pause.svg
new file mode 100644
index 000000000..ba12bc64c
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/voice_broadcast_record_pause.svg
@@ -0,0 +1,5 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/Contents.json
new file mode 100644
index 000000000..8431bfd58
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_stop.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/voice_broadcast_stop.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/voice_broadcast_stop.svg
new file mode 100644
index 000000000..1fed1640b
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/voice_broadcast_stop.svg
@@ -0,0 +1,6 @@
+
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index af7cab7a3..99c2c7041 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -338,6 +338,9 @@ internal class Asset: NSObject {
internal static let voiceBroadcastLive = ImageAsset(name: "voice_broadcast_live")
internal static let voiceBroadcastPause = ImageAsset(name: "voice_broadcast_pause")
internal static let voiceBroadcastPlay = ImageAsset(name: "voice_broadcast_play")
+ internal static let voiceBroadcastRecord = ImageAsset(name: "voice_broadcast_record")
+ internal static let voiceBroadcastRecordPause = ImageAsset(name: "voice_broadcast_record_pause")
+ internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop")
internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo")
}
@objcMembers
diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift
index 95b7bbaf4..35caf9084 100644
--- a/Riot/Modules/Room/RoomCoordinator.swift
+++ b/Riot/Modules/Room/RoomCoordinator.swift
@@ -93,6 +93,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
TimelinePollProvider.shared.session = parameters.session
VoiceBroadcastPlaybackProvider.shared.session = parameters.session
+ VoiceBroadcastRecorderProvider.shared.session = parameters.session
super.init()
}
diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m
index b9116fbd9..82d62b474 100644
--- a/Riot/Modules/Room/RoomViewController.m
+++ b/Riot/Modules/Room/RoomViewController.m
@@ -3272,6 +3272,22 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
}
+ else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcastRecord)
+ {
+ if (bubbleData.isPaginationFirstBubble)
+ {
+ cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle;
+ }
+ else if (bubbleData.shouldHideSenderInformation)
+ {
+ cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo;
+ }
+ else
+ {
+ cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder;
+ }
+ }
+
else if (roomBubbleCellData.getFirstBubbleComponentWithDisplay.event.isEmote)
{
if (bubbleData.isIncoming)
diff --git a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
index 640a2e3bc..3348df0e6 100644
--- a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
+++ b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
@@ -178,6 +178,11 @@ typedef NS_ENUM(NSUInteger, RoomTimelineCellIdentifier) {
RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo,
RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle,
+ // - Voice broadcast recorder
+ RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder,
+ RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo,
+ RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle,
+
// - Others
RoomTimelineCellIdentifierEmpty,
RoomTimelineCellIdentifierSelectedSticker,
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m
index 42bad501d..c747476ee 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m
@@ -143,6 +143,13 @@
[tableView registerClass:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier];
}
+- (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView
+{
+ // Outgoing
+ [tableView registerClass:VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier];
+ [tableView registerClass:VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier];
+}
+
#pragma mark - Mapping
- (NSDictionary*)incomingTextMessageCellsMapping
@@ -318,4 +325,14 @@
};
}
+- (NSDictionary*)voiceBroadcastRecorderCellsMapping
+{
+ return @{
+ // Outgoing
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo) : VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle) : VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.class,
+ };
+}
+
@end
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.swift
new file mode 100644
index 000000000..c30badc8e
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.swift
@@ -0,0 +1,27 @@
+//
+// Copyright 2022 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
+
+class VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell: VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showPaginationTitle = true
+ }
+
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.swift
new file mode 100644
index 000000000..4d56aee96
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.swift
@@ -0,0 +1,41 @@
+//
+// Copyright 2022 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
+
+class VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell: VoiceBroadcastRecorderBubbleCell, BubbleOutgoingRoomCellProtocol {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showSenderInfo = false
+
+ // TODO: VB update margins attributes
+ let leftMargin: CGFloat = BubbleRoomCellLayoutConstants.outgoingBubbleBackgroundMargins.left + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left
+ let rightMargin: CGFloat = BubbleRoomCellLayoutConstants.outgoingBubbleBackgroundMargins.right + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right
+
+ roomCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin
+ roomCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin
+
+ self.setupBubbleDecorations()
+ }
+
+ override func update(theme: Theme) {
+ super.update(theme: theme)
+
+ self.bubbleBackgroundColor = theme.roomCellOutgoingBubbleBackgroundColor
+ }
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderBubbleCell.swift
new file mode 100644
index 000000000..5b7a92a2f
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderBubbleCell.swift
@@ -0,0 +1,113 @@
+//
+// Copyright 2022 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 UIKit
+
+class VoiceBroadcastRecorderBubbleCell: VoiceBroadcastRecorderPlainCell {
+
+ // MARK: - Properties
+
+ var bubbleBackgroundColor: UIColor?
+
+ // MARK: - Overrides
+
+ override func render(_ cellData: MXKCellData!) {
+ super.render(cellData)
+
+ self.update(theme: ThemeService.shared().theme)
+ }
+
+ override func setupViews() {
+ super.setupViews()
+
+ self.setupBubbleBackgroundView()
+ }
+
+ override func addVoiceBroadcastView(_ voiceBroadcastView: UIView, on contentView: UIView) {
+ super.addVoiceBroadcastView(voiceBroadcastView, on: contentView)
+
+ self.addBubbleBackgroundViewIfNeeded(for: voiceBroadcastView)
+ }
+
+ // MARK: - Private
+
+ private func addBubbleBackgroundViewIfNeeded(for voiceBroadcastView: UIView) {
+
+ guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else {
+ return
+ }
+
+ self.addBubbleBackgroundView( messageBubbleBackgroundView, to: voiceBroadcastView)
+ messageBubbleBackgroundView.backgroundColor = self.bubbleBackgroundColor
+ }
+
+ private func addBubbleBackgroundView(_ bubbleBackgroundView: RoomMessageBubbleBackgroundView,
+ to voiceBroadcastView: UIView) {
+
+ // TODO: VB update margins attributes
+ let topMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.top
+ let leftMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left
+ let rightMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right
+ let bottomMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.bottom
+
+ let topAnchor = voiceBroadcastView.topAnchor
+ let leadingAnchor = voiceBroadcastView.leadingAnchor
+ let trailingAnchor = voiceBroadcastView.trailingAnchor
+ let bottomAnchor = voiceBroadcastView.bottomAnchor
+
+ NSLayoutConstraint.activate([
+ bubbleBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: -topMargin),
+ bubbleBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -leftMargin),
+ bubbleBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: rightMargin),
+ bubbleBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomMargin)
+ ])
+ }
+
+ private func setupBubbleBackgroundView() {
+ let bubbleBackgroundView = RoomMessageBubbleBackgroundView()
+ self.roomCellContentView?.insertSubview(bubbleBackgroundView, at: 0)
+ }
+
+ // The extension property MXKRoomBubbleTableViewCell.messageBubbleBackgroundView is not working there even by doing recursion
+ private func getBubbleBackgroundView() -> RoomMessageBubbleBackgroundView? {
+ guard let contentView = self.roomCellContentView else {
+ return nil
+ }
+
+ let foundView = contentView.subviews.first { view in
+ return view is RoomMessageBubbleBackgroundView
+ }
+ return foundView as? RoomMessageBubbleBackgroundView
+ }
+}
+
+// MARK: - TimestampDisplayable
+extension VoiceBroadcastRecorderBubbleCell: TimestampDisplayable {
+
+ func addTimestampView(_ timestampView: UIView) {
+ guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else {
+ return
+ }
+ messageBubbleBackgroundView.addTimestampView(timestampView)
+ }
+
+ func removeTimestampView() {
+ guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else {
+ return
+ }
+ messageBubbleBackgroundView.removeTimestampView()
+ }
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift
new file mode 100644
index 000000000..299179a94
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift
@@ -0,0 +1,65 @@
+//
+// Copyright 2022 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
+
+class VoiceBroadcastRecorderPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable {
+
+ private var voiceBroadcastView: UIView?
+ private var event: MXEvent?
+
+ override func render(_ cellData: MXKCellData!) {
+ super.render(cellData)
+
+ guard let contentView = roomCellContentView?.innerContentView,
+ let bubbleData = cellData as? RoomBubbleCellData,
+ let event = bubbleData.events.last,
+ let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content),
+ voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue,
+ let view = VoiceBroadcastRecorderProvider.shared.buildVoiceBroadcastRecorderViewForEvent(event) else {
+ return
+ }
+
+ self.event = event
+ self.addVoiceBroadcastView(view, on: contentView)
+ }
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.backgroundColor = .clear
+ roomCellContentView?.showSenderInfo = true
+ roomCellContentView?.showPaginationTitle = false
+ }
+
+ // The normal flow for tapping on cell content views doesn't work for bubbles without attributed strings
+ override func onContentViewTap(_ sender: UITapGestureRecognizer) {
+ guard let event = self.event else {
+ return
+ }
+
+ delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnContentView, userInfo: [kMXKRoomBubbleCellEventKey: event])
+ }
+
+ func addVoiceBroadcastView(_ voiceBroadcastView: UIView, on contentView: UIView) {
+
+ self.voiceBroadcastView?.removeFromSuperview()
+ contentView.vc_addSubViewMatchingParent(voiceBroadcastView)
+ self.voiceBroadcastView = voiceBroadcastView
+ }
+}
+
+extension VoiceBroadcastRecorderPlainCell: RoomCellThreadSummaryDisplayable {}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift
new file mode 100644
index 000000000..4247f306c
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift
@@ -0,0 +1,27 @@
+//
+// Copyright 2022 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
+
+class VoiceBroadcastRecorderWithPaginationTitlePlainCell: VoiceBroadcastRecorderPlainCell {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showPaginationTitle = true
+ }
+
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift
new file mode 100644
index 000000000..172b10aee
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift
@@ -0,0 +1,27 @@
+//
+// Copyright 2022 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
+
+class VoiceBroadcastRecorderWithoutSenderInfoPlainCell: VoiceBroadcastRecorderPlainCell {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showSenderInfo = false
+ }
+
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h
index 9f18a71d9..b1e85a621 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h
@@ -58,6 +58,8 @@ NS_ASSUME_NONNULL_BEGIN
- (NSDictionary*)voiceBroadcastCellsMapping;
+- (NSDictionary*)voiceBroadcastRecorderCellsMapping;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
index db11457d7..4813b539d 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
@@ -115,6 +115,8 @@
[self registerVoiceBroadcastCellsForTableView:tableView];
+ [self registerVoiceBroadcastRecorderCellsForTableView:tableView];
+
[tableView registerClass:RoomEmptyBubbleCell.class forCellReuseIdentifier:RoomEmptyBubbleCell.defaultReuseIdentifier];
[tableView registerClass:RoomSelectedStickerBubbleCell.class forCellReuseIdentifier:RoomSelectedStickerBubbleCell.defaultReuseIdentifier];
@@ -279,6 +281,13 @@
[tableView registerClass:VoiceBroadcastWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastWithPaginationTitlePlainCell.defaultReuseIdentifier];
}
+- (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView
+{
+ [tableView registerClass:VoiceBroadcastRecorderPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderPlainCell.defaultReuseIdentifier];
+ [tableView registerClass:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.defaultReuseIdentifier];
+ [tableView registerClass:VoiceBroadcastRecorderWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithPaginationTitlePlainCell.defaultReuseIdentifier];
+}
+
#pragma mark Cell class association
- (NSDictionary*)buildCellClasses
@@ -339,6 +348,9 @@
NSDictionary *voiceBroadcastCellsMapping = [self voiceBroadcastCellsMapping];
[cellClasses addEntriesFromDictionary:voiceBroadcastCellsMapping];
+
+ NSDictionary *voiceBroadcastRecorderCellsMapping = [self voiceBroadcastRecorderCellsMapping];
+ [cellClasses addEntriesFromDictionary:voiceBroadcastRecorderCellsMapping];
NSDictionary *othersCells = @{
@(RoomTimelineCellIdentifierEmpty) : RoomEmptyBubbleCell.class,
@@ -576,5 +588,14 @@
};
}
+- (NSDictionary*)voiceBroadcastRecorderCellsMapping
+{
+ return @{
+ // Outoing
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo) : VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle) : VoiceBroadcastRecorderWithPaginationTitlePlainCell.class
+ };
+}
@end
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift
new file mode 100644
index 000000000..7ac01946b
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift
@@ -0,0 +1,65 @@
+//
+// Copyright 2022 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 Combine
+import Foundation
+import SwiftUI
+import UIKit
+import AVFoundation
+
+struct VoiceBroadcastRecorderCoordinatorParameters {
+ let session: MXSession
+ let room: MXRoom
+ let voiceBroadcastStartEvent: MXEvent
+}
+
+final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: VoiceBroadcastRecorderCoordinatorParameters
+
+ private var voiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol
+ private var voiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelProtocol
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+
+ // MARK: - Setup
+
+ init(parameters: VoiceBroadcastRecorderCoordinatorParameters) {
+ self.parameters = parameters
+
+ voiceBroadcastRecorderService = VoiceBroadcastRecorderService(session: parameters.session, roomId: parameters.room.matrixItemId)
+
+ let viewModel = VoiceBroadcastRecorderViewModel(recorderService: voiceBroadcastRecorderService)
+ voiceBroadcastRecorderViewModel = viewModel
+ }
+
+ // MARK: - Public
+
+ func start() { }
+
+ func toPresentable() -> UIViewController {
+ VectorHostingController(rootView: VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context),
+ forceZeroSafeAreaInsets: true)
+ }
+
+ // MARK: - Private
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift
new file mode 100644
index 000000000..d162a3bf0
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift
@@ -0,0 +1,58 @@
+//
+// Copyright 2022 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 AVFoundation
+
+class VoiceBroadcastRecorderProvider {
+
+ // MARK: - Constants
+ static let shared = VoiceBroadcastRecorderProvider()
+
+ // MARK: - Properties
+ // MARK: Public
+ var session: MXSession?
+ var coordinatorsForEventIdentifiers = [String: VoiceBroadcastRecorderCoordinator]()
+
+ // MARK: - Setup
+ private init() { }
+
+ // MARK: - Public
+
+ /// Create or retrieve the voiceBroadcast timeline coordinator for this event and return
+ /// a view to be displayed in the timeline
+ func buildVoiceBroadcastRecorderViewForEvent(_ event: MXEvent) -> UIView? {
+ guard let session = session, let room = session.room(withRoomId: event.roomId) else {
+ return nil
+ }
+
+ if let coordinator = coordinatorsForEventIdentifiers[event.eventId] {
+ return coordinator.toPresentable().view
+ }
+
+ let parameters = VoiceBroadcastRecorderCoordinatorParameters(session: session, room: room, voiceBroadcastStartEvent: event)
+ let coordinator = VoiceBroadcastRecorderCoordinator(parameters: parameters)
+
+ coordinatorsForEventIdentifiers[event.eventId] = coordinator
+
+ return coordinator.toPresentable().view
+ }
+
+ /// Retrieve the voiceBroadcast timeline coordinator for the given event or nil if it hasn't been created yet
+ func voiceBroadcastRecorderControllerForEventIdentifier(_ eventIdentifier: String) -> VoiceBroadcastRecorderCoordinator? {
+ coordinatorsForEventIdentifiers[eventIdentifier]
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderService.swift
new file mode 100644
index 000000000..1fb292fe4
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderService.swift
@@ -0,0 +1,199 @@
+//
+// Copyright 2022 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 Combine
+import Foundation
+
+class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let roomId: String
+ private let session: MXSession
+ private var voiceBroadcastService: VoiceBroadcastService? {
+ session.voiceBroadcastService
+ }
+
+ private let audioEngine = AVAudioEngine()
+
+ private var chunkFile: AVAudioFile! = nil
+ private var chunkFrames: AVAudioFrameCount = 0
+ private var chunkFileNumber: Int = 0
+
+ // MARK: Public
+
+ // MARK: - Setup
+
+ init(session: MXSession, roomId: String) {
+ self.session = session
+ self.roomId = roomId
+ }
+
+ // MARK: - VoiceBroadcastRecorderServiceProtocol
+
+ func startRecordingVoiceBroadcast() {
+ let inputNode = audioEngine.inputNode
+
+ let inputBus = AVAudioNodeBus(0)
+ let inputFormat = inputNode.inputFormat(forBus: inputBus)
+ MXLog.debug("[VoiceBroadcastRecorderService] Start recording voice broadcast for bus name : \(String(describing: inputNode.name(forInputBus: inputBus)))")
+
+ inputNode.installTap(onBus: inputBus,
+ bufferSize: 512,
+ format: inputFormat) { (buffer, time) -> Void in
+ DispatchQueue.main.async {
+ self.writeBuffer(buffer)
+ }
+ }
+
+ // FIXME: Update state
+ try? audioEngine.start()
+ }
+
+ func stopRecordingVoiceBroadcast() {
+ audioEngine.stop()
+ audioEngine.reset() // FIXME: Really needed ?
+ resetValues()
+
+ voiceBroadcastService?.stopVoiceBroadcast(success: { _ in
+ // update recording state
+ }, failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to stop voice broadcast", context: error)
+ })
+ }
+
+ func pauseRecordingVoiceBroadcast() {
+ audioEngine.pause()
+
+ voiceBroadcastService?.pauseVoiceBroadcast(success: { _ in
+ // update recording state
+ }, failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to pause voice broadcast", context: error)
+ })
+ }
+
+ func resumeRecordingVoiceBroadcast() {
+ try? audioEngine.start() // FIXME: Verifiy if start is ok for a restart/resume
+
+ voiceBroadcastService?.resumeVoiceBroadcast(success: { _ in
+ // update recording state
+ }, failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to resume voice broadcast", context: error)
+ })
+ }
+
+ // MARK: - Private
+ /// Reset chunk values.
+ private func resetValues() {
+ chunkFrames = 0
+ chunkFileNumber = 0
+ }
+
+ /// Write audio buffer to chunk file.
+ private func writeBuffer(_ buffer: AVAudioPCMBuffer) {
+ let sampleRate = buffer.format.sampleRate
+
+ if chunkFile == nil {
+ createNewChunkFile(sampleRate: sampleRate)
+ }
+ try? chunkFile.write(from: buffer)
+
+ chunkFrames += buffer.frameLength
+
+ if chunkFrames > AVAudioFrameCount(Double(BuildSettings.voiceBroadcastChunkLength) * sampleRate) {
+ sendChunkFile(at: chunkFile.url)
+ // Reset chunkFile
+ chunkFile = nil
+ }
+ }
+
+ /// Create new chunk file with sample rate.
+ private func createNewChunkFile(sampleRate: Float64) {
+ guard let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
+ // FIXME: Manage error
+ return
+ }
+ let fileUrl = directory.appendingPathComponent("VoiceBroadcastChunk-\(roomId)-\(chunkFileNumber).m4a")
+ MXLog.debug("[VoiceBroadcastRecorderService] Create chunk file to \(fileUrl)")
+
+ let settings: [String: Any] = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+ AVSampleRateKey: sampleRate,
+ AVEncoderBitRateKey: 128000,
+ AVNumberOfChannelsKey: 1,
+ AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
+
+ chunkFile = try! AVAudioFile(forWriting: fileUrl, settings: settings)
+
+ chunkFileNumber += 1
+ chunkFrames = 0
+ }
+
+ /// Send chunk file to the server.
+ private func sendChunkFile(at url: URL) {
+ guard let voiceBroadcastService = voiceBroadcastService else {
+ // FIXME: Manage error
+ return
+ }
+
+ let dispatchGroup = DispatchGroup()
+ var duration = 0.0
+
+ dispatchGroup.enter()
+ VoiceMessageAudioConverter.mediaDurationAt(url) { result in
+ switch result {
+ case .success:
+ if let someDuration = try? result.get() {
+ duration = someDuration
+ } else {
+ MXLog.error("[VoiceBroadcastRecorderService] Failed retrieving media duration")
+ }
+ case .failure(let error):
+ MXLog.error("[VoiceBroadcastRecorderService] Failed getting audio duration", context: error)
+ }
+
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.notify(queue: .main) {
+ voiceBroadcastService.sendChunkOfVoiceBroadcast(audioFileLocalURL: url,
+ mimeType: "audio/mp4",
+ duration: UInt(duration * 1000),
+ samples: nil) { eventId in
+ MXLog.debug("[VoiceBroadcastRecorderService] sendChunkOfVoiceBroadcast success.")
+ if eventId != nil {
+ self.deleteRecording(at: url)
+ }
+ } failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] sendChunkOfVoiceBroadcast error.", context: error)
+ }
+ }
+ }
+
+ /// Delete voice broadcast chunk at URL.
+ private func deleteRecording(at url: URL?) {
+ guard let url = url else {
+ return
+ }
+
+ do {
+ try FileManager.default.removeItem(at: url)
+ } catch {
+ MXLog.error("[VoiceBroadcastRecorderService] deleteRecordingAtURL:", context: error)
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
new file mode 100644
index 000000000..ab033c127
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
@@ -0,0 +1,32 @@
+//
+// Copyright 2022 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 Combine
+import Foundation
+
+protocol VoiceBroadcastRecorderServiceProtocol {
+ /// Start voice broadcast recording.
+ func startRecordingVoiceBroadcast()
+
+ /// Stop voice broadcast recording.
+ func stopRecordingVoiceBroadcast()
+
+ /// Pause voice broadcast recording.
+ func pauseRecordingVoiceBroadcast()
+
+ /// Resume voice broadcast recording after paused it.
+ func resumeRecordingVoiceBroadcast()
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
new file mode 100644
index 000000000..693de5710
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
@@ -0,0 +1,84 @@
+//
+// Copyright 2022 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
+
+struct VoiceBroadcastRecorderView: View {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme: ThemeSwiftUI
+
+ // MARK: Public
+
+ @ObservedObject var viewModel: VoiceBroadcastRecorderViewModel.Context
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16.0) {
+ Text(VectorL10n.voiceBroadcastInTimelineTitle)
+ .font(theme.fonts.bodySB)
+ .foregroundColor(theme.colors.primaryContent)
+
+ HStack(alignment: .top, spacing: 16.0) {
+ Button {
+ // FIXME: Manage record in progress case
+ viewModel.send(viewAction: .start)
+ } label: {
+ // FIXME: Manage record in progress case
+ Image("voice_broadcast_record")
+ .renderingMode(.original)
+ }
+ .accessibilityIdentifier("recordButton")
+
+ Button {
+ // FIXME: Manage resume case
+ viewModel.send(viewAction: .pause)
+ } label: {
+ Image("voice_broadcast_record_pause")
+ .renderingMode(.original)
+ }
+ .accessibilityIdentifier("pauseButton")
+ }
+
+ }
+ .padding([.horizontal, .top], 2.0)
+ .padding([.bottom])
+ }
+
+// private func updateRecordingStatus() {
+// switch viewModel.viewState.recordingState {
+// case .started:
+// viewModel.send(viewAction: .stop)
+// case .paused:
+// viewModel.send(viewAction: .resume)
+// case .stopped:
+// viewModel.send(viewAction: .start)
+// case .resumed:
+// viewModel.send(viewAction: .pause)
+// }
+// }
+}
+
+
+// MARK: - Previews
+
+struct VoiceBroadcastRecorderView_Previews: PreviewProvider {
+ static let stateRenderer = MockVoiceBroadcastRecorderScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
new file mode 100644
index 000000000..4f811bcb7
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
@@ -0,0 +1,44 @@
+//
+// Copyright 2022 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 VoiceBroadcastRecorderViewAction {
+ case start
+ case stop
+ case pause
+ case resume
+}
+
+enum VoiceBroadcastRecorderState {
+ case started
+ case stopped
+ case paused
+ case resumed
+}
+
+struct VoiceBroadcastRecorderViewState: BindableState {
+ var recordingState: VoiceBroadcastRecorderState
+ var bindings: VoiceBroadcastRecorderViewStateBindings
+}
+
+struct VoiceBroadcastRecorderViewStateBindings {
+// var alertInfo: AlertInfo?
+}
+
+enum VoiceBroadcastRecorderAlertType {
+// case failedClosingVoiceBroadcast
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift
new file mode 100644
index 000000000..2afa3bf11
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift
@@ -0,0 +1,41 @@
+//
+// Copyright 2022 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
+
+typealias MockVoiceBroadcastRecorderViewModelType = StateStoreViewModel
+class MockVoiceBroadcastRecorderViewModel: MockVoiceBroadcastRecorderViewModelType, VoiceBroadcastRecorderViewModelProtocol {
+
+}
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable {
+
+ var screenType: Any.Type {
+ VoiceBroadcastRecorderView.self
+ }
+
+ var screenView: ([Any], AnyView) {
+ let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(recordingState: .stopped, bindings: VoiceBroadcastRecorderViewStateBindings()))
+
+ return (
+ [false, viewModel],
+ AnyView(VoiceBroadcastRecorderView(viewModel: viewModel.context))
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
new file mode 100644
index 000000000..8200aa994
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
@@ -0,0 +1,74 @@
+//
+// Copyright 2022 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 Combine
+import SwiftUI
+
+typealias VoiceBroadcastRecorderViewModelType = StateStoreViewModel
+
+class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, VoiceBroadcastRecorderViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let voiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol
+
+ // MARK: Public
+
+ // MARK: - Setup
+
+ init(recorderService: VoiceBroadcastRecorderServiceProtocol) {
+ self.voiceBroadcastRecorderService = recorderService
+ super.init(initialViewState: VoiceBroadcastRecorderViewState(recordingState: .stopped, bindings: VoiceBroadcastRecorderViewStateBindings()))
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: VoiceBroadcastRecorderViewAction) {
+ switch viewAction {
+ case .start:
+ start()
+ case .stop:
+ stop()
+ case .pause:
+ pause()
+ case .resume:
+ resume()
+ }
+ }
+
+ // MARK: - Private
+ private func start() {
+ self.state.recordingState = .started
+ voiceBroadcastRecorderService.startRecordingVoiceBroadcast()
+ }
+
+ private func stop() {
+ self.state.recordingState = .stopped
+ voiceBroadcastRecorderService.stopRecordingVoiceBroadcast()
+ }
+
+ private func pause() {
+ self.state.recordingState = .paused
+ voiceBroadcastRecorderService.pauseRecordingVoiceBroadcast()
+ }
+
+ private func resume() {
+ self.state.recordingState = .resumed
+ voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModelProtocol.swift
new file mode 100644
index 000000000..ab1e74c89
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModelProtocol.swift
@@ -0,0 +1,21 @@
+//
+// Copyright 2022 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 VoiceBroadcastRecorderViewModelProtocol {
+ var context: VoiceBroadcastRecorderViewModelType.Context { get }
+}