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