Add voice broadcast state event (#6785)

This commit is contained in:
Yoan Pintas 2022-10-12 11:30:38 +02:00 committed by GitHub
parent 2045a6d2c8
commit 3488b67976
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 781 additions and 101 deletions

View file

@ -30,9 +30,10 @@ class AppConfiguration: CommonConfiguration {
// Enable CallKit for app
MXKAppSettings.standard()?.isCallKitEnabled = true
// Get modular widget events in rooms histories
// Get additional events (modular widget, voice broadcast...)
MXKAppSettings.standard()?.addSupportedEventTypes([kWidgetMatrixEventTypeString,
kWidgetModularEventTypeString])
kWidgetModularEventTypeString,
VoiceBroadcastSettings.eventType])
// Hide undecryptable messages that were sent while the user was not in the room
MXKAppSettings.standard()?.hidePreJoinedUndecryptableEvents = true

View file

@ -404,6 +404,11 @@ final class BuildSettings: NSObject {
static let defaultTileServerMapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
static let locationSharingEnabled = true
// MARK: - Voice Broadcast
static let voiceBroadcastEnabled = false
static let voiceBroadcastChunkLength: Int = 600
static let voiceBroadcastMaxLength: Int = 144000
// MARK: - MXKAppSettings
static let enableBotCreation: Bool = false

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "action_live.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.1893 4.18664C19.8502 3.7507 19.222 3.67216 18.786 4.01123C18.3505 4.34993 18.2717 4.97719 18.6095 5.41308L18.6102 5.41406L18.6112 5.41535L18.6249 5.43351C18.6381 5.45131 18.6595 5.48037 18.6877 5.52021C18.7444 5.59993 18.8286 5.72248 18.9313 5.88389C19.137 6.20722 19.415 6.68336 19.6938 7.28061C20.254 8.48115 20.7999 10.134 20.7999 12.0006C20.7999 13.8672 20.254 15.52 19.6938 16.7206C19.415 17.3178 19.137 17.7939 18.9313 18.1173C18.8286 18.2787 18.7444 18.4012 18.6877 18.481C18.6595 18.5208 18.6381 18.5499 18.6249 18.5677L18.6112 18.5858L18.6102 18.5871L18.6093 18.5883C18.2717 19.0241 18.3506 19.6513 18.786 19.9899C19.222 20.329 19.8502 20.2505 20.1893 19.8145L19.444 19.2349C20.1893 19.8145 20.1893 19.8145 20.1893 19.8145L20.1913 19.812L20.1942 19.8082L20.2025 19.7974L20.2289 19.7622C20.2508 19.7328 20.2811 19.6915 20.3184 19.639C20.393 19.5339 20.4963 19.3832 20.6186 19.191C20.8629 18.8072 21.1849 18.2548 21.5061 17.5663C22.1459 16.1954 22.7999 14.2483 22.7999 12.0006C22.7999 9.75288 22.1459 7.80573 21.5061 6.43484C21.1849 5.74638 20.8629 5.19394 20.6186 4.81014C20.4963 4.61797 20.393 4.46731 20.3184 4.36221C20.2811 4.30963 20.2508 4.26839 20.2289 4.23898L20.2025 4.20377L20.1942 4.19292L20.1913 4.18921L20.1902 4.18778C20.1902 4.18778 20.1893 4.18664 19.3999 4.80058L20.1893 4.18664Z" fill="#0DBD8B"/>
<path d="M17.5902 7.78686C17.2511 7.35091 16.6228 7.27238 16.1869 7.61145C15.7521 7.94964 15.6728 8.57552 16.0088 9.01132L16.0123 9.0159C16.0167 9.02189 16.0252 9.03338 16.0371 9.05012C16.0609 9.08363 16.0982 9.13788 16.1447 9.21089C16.2379 9.35744 16.3659 9.57643 16.4946 9.85226C16.7549 10.4099 17.0008 11.1628 17.0008 12.0008C17.0008 12.8388 16.7549 13.5917 16.4946 14.1493C16.3659 14.4252 16.2379 14.6442 16.1447 14.7907C16.0982 14.8637 16.0609 14.918 16.0371 14.9515C16.0252 14.9682 16.0167 14.9797 16.0123 14.9857L16.0088 14.9903C15.6728 15.4261 15.7521 16.052 16.1869 16.3902C16.6228 16.7292 17.2511 16.6507 17.5902 16.2147L16.8008 15.6008C17.5902 16.2147 17.5902 16.2147 17.5902 16.2147L17.592 16.2125L17.5942 16.2096L17.5999 16.2021L17.6163 16.1802C17.6295 16.1626 17.6468 16.139 17.6677 16.1095C17.7095 16.0506 17.766 15.9682 17.832 15.8645C17.9637 15.6574 18.1357 15.3621 18.307 14.9951C18.6468 14.2671 19.0008 13.2199 19.0008 12.0008C19.0008 10.7817 18.6468 9.73452 18.307 9.00649C18.1357 8.63946 17.9637 8.34416 17.832 8.13714C17.766 8.03337 17.7095 7.95101 17.6677 7.89211C17.6468 7.86264 17.6295 7.83897 17.6163 7.82137L17.5999 7.7995L17.5942 7.79202L17.592 7.78915L17.591 7.78793C17.591 7.78793 17.5902 7.78686 16.8008 8.4008L17.5902 7.78686Z" fill="#0DBD8B"/>
<path d="M3.6105 19.8145C3.94957 20.2504 4.57785 20.3289 5.0138 19.9899C5.44926 19.6512 5.52809 19.0239 5.19033 18.588L5.18957 18.587L5.18859 18.5857L5.17495 18.5676C5.16169 18.5498 5.14035 18.5207 5.11206 18.4809C5.05545 18.4012 4.97123 18.2786 4.86852 18.1172C4.66276 17.7939 4.38476 17.3177 4.10604 16.7205C3.54579 15.5199 2.99986 13.8671 2.99986 12.0005C2.99986 10.1339 3.54579 8.48109 4.10604 7.28055C4.38476 6.68329 4.66276 6.20716 4.86852 5.88382C4.97123 5.72242 5.05545 5.59986 5.11206 5.52014C5.14035 5.48031 5.16169 5.45124 5.17495 5.43344L5.18859 5.41529L5.18957 5.414L5.19047 5.41283C5.52808 4.97695 5.4492 4.34981 5.0138 4.01117C4.57785 3.6721 3.94957 3.75063 3.6105 4.18658L4.35576 4.76622C3.6105 4.18659 3.6105 4.18658 3.6105 4.18658L3.60851 4.18914L3.60565 4.19286L3.59735 4.20371L3.57086 4.23891C3.54896 4.26832 3.51874 4.30957 3.4814 4.36214C3.40676 4.46725 3.30348 4.61791 3.18119 4.81007C2.93695 5.19388 2.61495 5.74632 2.29367 6.43478C1.65393 7.80566 0.999856 9.75282 0.999856 12.0005C0.999856 14.2482 1.65393 16.1954 2.29367 17.5663C2.61495 18.2547 2.93695 18.8072 3.18119 19.191C3.30348 19.3831 3.40676 19.5338 3.4814 19.6389C3.51874 19.6915 3.54896 19.7327 3.57086 19.7621L3.59735 19.7973L3.60565 19.8082L3.60851 19.8119L3.60962 19.8133C3.60962 19.8133 3.6105 19.8145 4.39986 19.2005L3.6105 19.8145Z" fill="#0DBD8B"/>
<path d="M6.20962 16.2142C6.5487 16.6502 7.17697 16.7287 7.61292 16.3896C8.04773 16.0515 8.12699 15.4256 7.79098 14.9898L7.78754 14.9852C7.78308 14.9792 7.77463 14.9677 7.76274 14.951C7.73894 14.9175 7.7016 14.8632 7.65514 14.7902C7.56188 14.6437 7.43388 14.4247 7.30516 14.1488C7.04491 13.5912 6.79898 12.8383 6.79898 12.0003C6.79898 11.1623 7.04491 10.4094 7.30516 9.85175C7.43388 9.57593 7.56188 9.35694 7.65514 9.21039C7.7016 9.13737 7.73894 9.08312 7.76274 9.04961C7.77463 9.03288 7.78308 9.02139 7.78754 9.01539L7.79099 9.01081C8.12699 8.575 8.04772 7.94913 7.61292 7.61095C7.17697 7.27188 6.5487 7.35041 6.20962 7.78636L6.99898 8.4003C6.20962 7.78636 6.20962 7.78636 6.20962 7.78636L6.20785 7.78864L6.20564 7.79151L6.19991 7.799L6.18346 7.82086C6.17034 7.83847 6.15302 7.86214 6.13209 7.89161C6.09026 7.95051 6.03385 8.03286 5.96781 8.13664C5.83607 8.34366 5.66408 8.63895 5.49279 9.00598C5.15305 9.73401 4.79898 10.7812 4.79898 12.0003C4.79898 13.2194 5.15305 14.2666 5.49279 14.9946C5.66408 15.3616 5.83607 15.6569 5.96781 15.864C6.03385 15.9677 6.09026 16.0501 6.13209 16.109C6.15302 16.1385 6.17034 16.1621 6.18346 16.1797L6.19991 16.2016L6.20564 16.2091L6.20785 16.212L6.2088 16.2132C6.2088 16.2132 6.20962 16.2142 6.99898 15.6003L6.20962 16.2142Z" fill="#0DBD8B"/>
<circle cx="12" cy="12" r="2" fill="#0DBD8B"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -178,6 +178,7 @@ internal class Asset: NSObject {
internal static let peopleFloatingAction = ImageAsset(name: "people_floating_action")
internal static let actionCamera = ImageAsset(name: "action_camera")
internal static let actionFile = ImageAsset(name: "action_file")
internal static let actionLive = ImageAsset(name: "action_live")
internal static let actionLocation = ImageAsset(name: "action_location")
internal static let actionMediaLibrary = ImageAsset(name: "action_media_library")
internal static let actionPoll = ImageAsset(name: "action_poll")

View file

@ -36,7 +36,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag)
RoomBubbleCellDataTagRoomCreationIntro,
RoomBubbleCellDataTagPoll,
RoomBubbleCellDataTagLocation,
RoomBubbleCellDataTagLiveLocation
RoomBubbleCellDataTagLiveLocation,
RoomBubbleCellDataTagVoiceBroadcast
};
/**

View file

@ -182,6 +182,13 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
// Show timestamps always on right
self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO;
}
} else if ([event.type isEqualToString:VoiceBroadcastSettings.eventType]) {
self.tag = RoomBubbleCellDataTagVoiceBroadcast;
self.collapsable = NO;
self.collapsed = NO;
MXLogDebug(@"VB incoming initWithEvent")
break;
}
break;
@ -271,42 +278,44 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
- (BOOL)hasNoDisplay
{
if (self.tag == RoomBubbleCellDataTagKeyVerificationNoDisplay)
BOOL hasNoDisplay = YES;
switch (self.tag)
{
return YES;
case RoomBubbleCellDataTagKeyVerificationNoDisplay:
hasNoDisplay = YES;
break;
case RoomBubbleCellDataTagRoomCreationIntro:
hasNoDisplay = NO;
break;
case RoomBubbleCellDataTagPoll:
if (self.events.lastObject.isEditEvent) {
hasNoDisplay = YES;
}
hasNoDisplay = NO;
break;
case RoomBubbleCellDataTagLocation:
hasNoDisplay = NO;
break;
case RoomBubbleCellDataTagLiveLocation:
// If the summary does not exist don't show the cell
if (!self.beaconInfoSummary)
{
hasNoDisplay = YES;
}
hasNoDisplay = NO;
break;
case RoomBubbleCellDataTagVoiceBroadcast:
hasNoDisplay = YES;
break;
default:
hasNoDisplay = [super hasNoDisplay];
break;
}
if (self.tag == RoomBubbleCellDataTagRoomCreationIntro)
{
return NO;
}
if (self.tag == RoomBubbleCellDataTagPoll)
{
if (self.events.lastObject.isEditEvent) {
return YES;
}
return NO;
}
if (self.tag == RoomBubbleCellDataTagLocation)
{
return NO;
}
if (self.tag == RoomBubbleCellDataTagLiveLocation)
{
// If the summary does not exist don't show the cell
if (!self.beaconInfoSummary)
{
return YES;
}
return NO;
}
return [super hasNoDisplay];
return hasNoDisplay;
}
- (BOOL)hasThreadRoot
@ -1050,6 +1059,9 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
case RoomBubbleCellDataTagLiveLocation:
shouldAddEvent = NO;
break;
case RoomBubbleCellDataTagVoiceBroadcast:
shouldAddEvent = NO;
break;
default:
break;
}
@ -1118,6 +1130,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
{
shouldAddEvent = NO;
}
} else if ([event.type isEqualToString:VoiceBroadcastSettings.eventType]) {
shouldAddEvent = NO;
}
break;
}

View file

@ -197,8 +197,6 @@ extension RoomDataSource {
return editableTextMessage
}
}
// MARK: - Private Helpers

View file

@ -2308,6 +2308,35 @@ static CGSize kThreadListBarButtonItemImageSize;
[self showCameraControllerAnimated:YES];
}]];
}
if (BuildSettings.voiceBroadcastEnabled && !self.isNewDirectChat)
{
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionLive.image andAction:^{
MXStrongifyAndReturnIfNil(self);
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
}
// TODO: Init and start voice broadcast
MXSession* session = self.roomDataSource.mxSession;
[session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) {
if (voiceBroadcastService) {
if ([[voiceBroadcastService getState] isEqualToString:@"stopped"]) {
[session.voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) {
} failure:^(NSError * _Nonnull error) {
}];
} else {
[session.voiceBroadcastService stopVoiceBroadcastWithSuccess:^(NSString * _Nullable success) {
} failure:^(NSError * _Nonnull error) {
}];
}
}
}];
}]];
}
roomInputView.actionsBar.actionItems = actionItems;
}

View file

@ -0,0 +1,31 @@
//
// 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 MatrixSDK
extension MXSession {
/// Convenient getter to retrieve VoiceBroadcastService associated to the session
@objc var voiceBroadcastService: VoiceBroadcastService? {
return VoiceBroadcastServiceProvider.shared.currentVoiceBroadcastService
}
/// Initialize VoiceBroadcastService
@objc public func getOrCreateVoiceBroadcastService(for room: MXRoom, completion: @escaping (VoiceBroadcastService?) -> Void) {
VoiceBroadcastServiceProvider.shared.getOrCreateVoiceBroadcastService(for: room, completion: completion)
}
}

View file

@ -0,0 +1,40 @@
//
// 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 <MatrixSDK/MatrixSDK.h>
#import "MXJSONModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface VoiceBroadcastEventContent : MXJSONModel
/// The voice broadcast state (started - paused - resumed - stopped).
@property (nonatomic) NSString *state;
/// The length of the voice chunks in seconds. Only required on the started state event.
@property (nonatomic) NSInteger chunkLength;
/// The event id of the started voice broadcast info state event.
@property (nonatomic, strong, nullable) NSString* eventId;
- (instancetype)initWithState:(NSString *)state
chunkLength:(NSInteger)chunkLength
eventId:(NSString *)eventId;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,79 @@
//
// 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 "VoiceBroadcastEventContent.h"
#import "GeneratedInterface-Swift.h"
@implementation VoiceBroadcastEventContent
- (instancetype)initWithState:(NSString *)state
chunkLength:(NSInteger)chunkLength
eventId:(NSString *)eventId
{
if (self = [super init])
{
_state = state;
_chunkLength = chunkLength;
_eventId = eventId;
}
return self;
}
+ (id)modelFromJSON:(NSDictionary *)JSONDictionary
{
NSString *state;
MXJSONModelSetString(state, JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyState]);
NSInteger chunkLength = BuildSettings.voiceBroadcastChunkLength;
if (JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLength])
{
MXJSONModelSetInteger(chunkLength, JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLength]);
}
NSString *eventId;
if (JSONDictionary[kMXEventRelationRelatesToKey]) {
MXEventContentRelatesTo *relatesTo;
MXJSONModelSetMXJSONModel(relatesTo, MXEventContentRelatesTo, JSONDictionary[kMXEventRelationRelatesToKey]);
if (relatesTo && [relatesTo.relationType isEqualToString:MXEventRelationTypeReference])
{
eventId = relatesTo.eventId;
}
}
return [[VoiceBroadcastEventContent alloc] initWithState:state chunkLength:chunkLength eventId:eventId];
}
- (NSDictionary *)JSONDictionary
{
NSMutableDictionary *JSONDictionary = [NSMutableDictionary dictionary];
JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyState] = self.state;
if (_eventId) {
MXEventContentRelatesTo *relatesTo = [[MXEventContentRelatesTo alloc] initWithRelationType:MXEventRelationTypeReference eventId:_eventId];
JSONDictionary[kMXEventRelationRelatesToKey] = relatesTo.JSONDictionary;
} else {
JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLength] = @(self.chunkLength);
}
return JSONDictionary;
}
@end

View file

@ -0,0 +1,296 @@
//
// 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
/// Voice Broadcast settings.
@objcMembers
final class VoiceBroadcastSettings: NSObject {
static let eventType = "io.element.voice_broadcast_info"
static let voiceBroadcastContentKeyState = "state"
static let voiceBroadcastContentKeyChunkLength = "chunk_length"
}
/// VoiceBroadcastService handles voice broadcast.
/// Note: Cannot use a protocol because of Objective-C compatibility
@objcMembers
public class VoiceBroadcastService: NSObject {
// MARK: - Properties
private var voiceBroadcastInfoEventId: String?
public let room: MXRoom
public private(set) var state: State
// MARK: - Setup
public init(room: MXRoom, state: State) {
self.room = room
self.state = state
}
// MARK: - Constants
public enum State: String {
case started
case paused
case resumed
case stopped
}
// MARK: - Public
// MARK: Voice broadcast info
/// Start a voice broadcast.
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
/// - Returns: a `MXHTTPOperation` instance.
func startVoiceBroadcast(completion: @escaping (MXResponse<String?>) -> Void) -> MXHTTPOperation? {
return sendVoiceBroadcastInfo(state: State.started) { [weak self] response in
guard let self = self else { return }
switch response {
case .success((let eventIdResponse)):
self.voiceBroadcastInfoEventId = eventIdResponse
completion(.success(eventIdResponse))
case .failure(let error):
completion(.failure(error))
}
}
}
/// Pause a voice broadcast.
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
/// - Returns: a `MXHTTPOperation` instance.
func pauseVoiceBroadcast(completion: @escaping (MXResponse<String?>) -> Void) -> MXHTTPOperation? {
return sendVoiceBroadcastInfo(state: State.paused, completion: completion)
}
/// resume a voice broadcast.
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
/// - Returns: a `MXHTTPOperation` instance.
func resumeVoiceBroadcast(completion: @escaping (MXResponse<String?>) -> Void) -> MXHTTPOperation? {
return sendVoiceBroadcastInfo(state: State.resumed, completion: completion)
}
/// stop a voice broadcast info.
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
/// - Returns: a `MXHTTPOperation` instance.
func stopVoiceBroadcast(completion: @escaping (MXResponse<String?>) -> Void) -> MXHTTPOperation? {
return sendVoiceBroadcastInfo(state: State.stopped, completion: completion)
}
func getState() -> String {
return self.state.rawValue
}
// MARK: Voice broadcast chunk
/// Send a bunch of a voice broadcast.
///
/// While sending, a fake event will be echoed in the messages list.
/// Once complete, this local echo will be replaced by the event saved by the homeserver.
///
/// - Parameters:
/// - audioFileLocalURL: the local filesystem path of the audio file to send.
/// - mimeType: (optional) the mime type of the file. Defaults to `audio/ogg`
/// - duration: the length of the voice message in milliseconds
/// - samples: an array of floating point values normalized to [0, 1], boxed within NSNumbers
/// - success: A block object called when the operation succeeds. It returns the event id of the event generated on the homeserver
/// - failure: A block object called when the operation fails.
func sendChunkOfVoiceBroadcast(audioFileLocalURL: URL,
mimeType: String?,
duration: UInt,
samples: [Float]?,
success:@escaping ((String?) -> Void),
failure:@escaping ((Error?) -> Void)) {
guard let voiceBroadcastInfoEventId = self.voiceBroadcastInfoEventId else {
return failure(VoiceBroadcastServiceError.notStarted)
}
self.room.sendChunkOfVoiceBroadcast(localURL: audioFileLocalURL,
voiceBroadcastInfoEventId: voiceBroadcastInfoEventId,
mimeType: mimeType,
duration: duration,
samples: samples,
success: success,
failure: failure)
}
// MARK: - Private
private func sendVoiceBroadcastInfo(state: State, completion: @escaping (MXResponse<String?>) -> Void) -> MXHTTPOperation? {
guard let userId = self.room.mxSession.myUserId else {
completion(.failure(VoiceBroadcastServiceError.missingUserId))
return nil
}
let stateKey = userId
let voiceBroadcastContent = VoiceBroadcastEventContent()
voiceBroadcastContent.state = state.rawValue
if state != State.started {
guard let voiceBroadcastInfoEventId = self.voiceBroadcastInfoEventId else {
completion(.failure(VoiceBroadcastServiceError.notStarted))
return nil
}
voiceBroadcastContent.eventId = voiceBroadcastInfoEventId
} else {
voiceBroadcastContent.chunkLength = BuildSettings.voiceBroadcastChunkLength
}
guard let stateEventContent = voiceBroadcastContent.jsonDictionary() as? [String: Any] else {
completion(.failure(VoiceBroadcastServiceError.unknown))
return nil
}
return self.room.sendStateEvent(.custom(VoiceBroadcastSettings.eventType),
content: stateEventContent, stateKey: stateKey) { [weak self] response in
guard let self = self else { return }
switch response {
case .success(let object):
self.state = state
completion(.success(object))
case .failure(let error):
completion(.failure(error))
}
}
}
}
// MARK: - Objective-C interface
extension VoiceBroadcastService {
/// Start a voice broadcast.
/// - Parameters:
/// - success: A closure called when the operation is complete.
/// - failure: A closure called when the operation fails.
/// - Returns: a `MXHTTPOperation` instance.
@discardableResult
@objc public func startVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? {
return self.startVoiceBroadcast() { (response) in
switch response {
case .success(let object):
success(object)
case .failure(let error):
failure(error)
}
}
}
/// Pause a voice broadcast.
/// - Parameters:
/// - success: A closure called when the operation is complete.
/// - failure: A closure called when the operation fails.
/// - Returns: a `MXHTTPOperation` instance.
@discardableResult
@objc public func pauseVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? {
return self.pauseVoiceBroadcast() { (response) in
switch response {
case .success(let object):
success(object)
case .failure(let error):
failure(error)
}
}
}
/// Resume a voice broadcast.
/// - Parameters:
/// - success: A closure called when the operation is complete.
/// - failure: A closure called when the operation fails.
/// - Returns: a `MXHTTPOperation` instance.
@discardableResult
@objc public func resumeVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? {
return self.resumeVoiceBroadcast() { (response) in
switch response {
case .success(let object):
success(object)
case .failure(let error):
failure(error)
}
}
}
/// Stop a voice broadcast.
/// - Parameters:
/// - success: A closure called when the operation is complete.
/// - failure: A closure called when the operation fails.
/// - Returns: a `MXHTTPOperation` instance.
@discardableResult
@objc public func stopVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? {
return self.stopVoiceBroadcast() { (response) in
switch response {
case .success(let object):
success(object)
case .failure(let error):
failure(error)
}
}
}
}
// MARK: - Internal room additions
extension MXRoom {
/// Send a voice broadcast to the room.
/// - Parameters:
/// - localURL: the local filesystem path of the file to send.
/// - voiceBroadcastInfoEventId: The id of the voice broadcast info event.
/// - mimeType: (optional) the mime type of the file. Defaults to `audio/ogg`.
/// - duration: the length of the voice message in milliseconds
/// - samples: an array of floating point values normalized to [0, 1]
/// - threadId: the id of the thread to send the message. nil by default.
/// - success: A closure called when the operation is complete.
/// - failure: A closure called when the operation fails.
/// - Returns: a `MXHTTPOperation` instance.
@nonobjc @discardableResult func sendChunkOfVoiceBroadcast(localURL: URL,
voiceBroadcastInfoEventId: String,
mimeType: String?,
duration: UInt,
samples: [Float]?,
threadId: String? = nil,
success: @escaping ((String?) -> Void),
failure: @escaping ((Error?) -> Void)) -> MXHTTPOperation? {
let boxedSamples = samples?.compactMap { NSNumber(value: $0) }
guard let relatesTo = MXEventContentRelatesTo(relationType: MXEventRelationTypeReference,
eventId: voiceBroadcastInfoEventId).jsonDictionary() as? [String: Any] else {
failure(VoiceBroadcastServiceError.unknown)
return nil
}
return __sendVoiceMessage(localURL,
additionalContentParams: [kMXEventRelationRelatesToKey: relatesTo],
mimeType: mimeType,
duration: duration,
samples: boxedSamples,
threadId: threadId,
localEcho: nil,
success: success,
failure: failure,
keepActualFilename: false)
}
}

View file

@ -0,0 +1,38 @@
//
// 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
/// VoiceBroadcastService error
public enum VoiceBroadcastServiceError: Int, Error {
case missingUserId
case roomNotFound
case notStarted
case unknown
}
// MARK: - VoiceBroadcastService errors
extension VoiceBroadcastServiceError: CustomNSError {
public static let errorDomain = "io.element.voice_broadcast_info"
public var errorCode: Int {
return Int(rawValue)
}
public var errorUserInfo: [String: Any] {
return [:]
}
}

View file

@ -0,0 +1,120 @@
//
// 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
/// VoiceBroadcastServiceProvider to setup VoiceBroadcastService or retrieve the existing VoiceBroadcastService.
class VoiceBroadcastServiceProvider {
// MARK: - Constants
static let shared = VoiceBroadcastServiceProvider()
// MARK: - Properties
/// VoiceBroadcastService in the current session
public var currentVoiceBroadcastService: VoiceBroadcastService?
// MARK: - Setup
private init() {}
// MARK: - Public
public func getOrCreateVoiceBroadcastService(for room: MXRoom, completion: @escaping (VoiceBroadcastService?) -> Void) {
guard let voiceBroadcastService = self.currentVoiceBroadcastService else {
self.setupVoiceBroadcastService(for: room) { voiceBroadcastService in
completion(voiceBroadcastService)
}
return
}
if voiceBroadcastService.room.roomId == room.roomId {
completion(voiceBroadcastService)
}
completion(nil)
}
public func tearDownVoiceBroadcastService() {
self.currentVoiceBroadcastService = nil
MXLog.debug("Stop monitoring voice broadcast recording")
}
// MARK: - Private
// MARK: VoiceBroadcastService setup
/// Get latest voice broadcast info in a room
/// - Parameters:
/// - room: The room.
/// - completion: Completion block that will return the lastest voice broadcast info state event of the room.
private func getLastVoiceBroadcastInfo(for room: MXRoom, completion: @escaping (MXEvent?) -> Void) {
room.state { roomState in
completion(roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.eventType))?.last ?? nil)
}
}
private func createVoiceBroadcastService(for room: MXRoom, state: VoiceBroadcastService.State) {
let voiceBroadcastService = VoiceBroadcastService(room: room, state: VoiceBroadcastService.State.stopped)
self.currentVoiceBroadcastService = voiceBroadcastService
MXLog.debug("Start monitoring voice broadcast recording")
}
/// Setup the voice broadcast service if no service is running locally.
///
/// A voice broadcast service is created in the following cases :
/// - A voice broadcast info state event doesn't exist in the room.
/// - The last voice broadcast info state event doesn't contain a valid content.
/// - The state of the last voice broadcast info state event is stopped.
/// - The state of the last voice broadcast info state event started by the end user is not stopped.
/// This may be due the following situations the application crashed or the voice broadcast has been started from another session.
///
/// - Parameters:
/// - room: The room.
/// - completion: Completion block that will return the voice broadcast service.
private func setupVoiceBroadcastService(for room: MXRoom, completion: @escaping (VoiceBroadcastService?) -> Void) {
self.getLastVoiceBroadcastInfo(for: room) { event in
guard let voiceBroadcastInfoEvent = event else {
self.createVoiceBroadcastService(for: room, state: VoiceBroadcastService.State.stopped)
completion(self.currentVoiceBroadcastService)
return
}
guard let voiceBroadcastInfoEventContent = VoiceBroadcastEventContent(fromJSON: voiceBroadcastInfoEvent.content) else {
self.createVoiceBroadcastService(for: room, state: VoiceBroadcastService.State.stopped)
completion(self.currentVoiceBroadcastService)
return
}
if voiceBroadcastInfoEventContent.state == VoiceBroadcastService.State.stopped.rawValue {
self.createVoiceBroadcastService(for: room, state: VoiceBroadcastService.State.stopped)
completion(self.currentVoiceBroadcastService)
} else if voiceBroadcastInfoEvent.stateKey == room.mxSession.myUserId {
self.createVoiceBroadcastService(for: room, state: VoiceBroadcastService.State(rawValue: voiceBroadcastInfoEventContent.state) ?? VoiceBroadcastService.State.stopped)
completion(self.currentVoiceBroadcastService)
} else {
completion(nil)
}
}
}
}

View file

@ -51,6 +51,7 @@
#import "RoomSelectedStickerBubbleCell.h"
#import "MXRoom+Riot.h"
#import "UniversalLink.h"
#import "VoiceBroadcastEventContent.h"
// MatrixKit common imports, shared with all targets
#import "MatrixKit-Bridging-Header.h"

View file

@ -181,95 +181,99 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
}
BOOL isEventSenderMyUser = [event.sender isEqualToString:mxSession.myUserId];
// Build strings for widget events
if (event.eventType == MXEventTypeCustom
&& ([event.type isEqualToString:kWidgetMatrixEventTypeString]
|| [event.type isEqualToString:kWidgetModularEventTypeString]))
{
NSString *displayText;
Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:mxSession];
if (widget)
if (event.eventType == MXEventTypeCustom) {
// Build strings for widget events
if ([event.type isEqualToString:kWidgetMatrixEventTypeString]
|| [event.type isEqualToString:kWidgetModularEventTypeString])
{
// Prepare the display name of the sender
NSString *senderDisplayName = roomState ? [self senderDisplayNameForEvent:event withRoomState:roomState] : event.sender;
if (widget.isActive)
NSString *displayText;
Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:mxSession];
if (widget)
{
if ([widget.type isEqualToString:kWidgetTypeJitsiV1]
|| [widget.type isEqualToString:kWidgetTypeJitsiV2])
// Prepare the display name of the sender
NSString *senderDisplayName = roomState ? [self senderDisplayNameForEvent:event withRoomState:roomState] : event.sender;
if (widget.isActive)
{
// This is an alive jitsi widget
if (isEventSenderMyUser)
if ([widget.type isEqualToString:kWidgetTypeJitsiV1]
|| [widget.type isEqualToString:kWidgetTypeJitsiV2])
{
displayText = [VectorL10n eventFormatterJitsiWidgetAddedByYou];
// This is an alive jitsi widget
if (isEventSenderMyUser)
{
displayText = [VectorL10n eventFormatterJitsiWidgetAddedByYou];
}
else
{
displayText = [VectorL10n eventFormatterJitsiWidgetAdded:senderDisplayName];
}
}
else
{
displayText = [VectorL10n eventFormatterJitsiWidgetAdded:senderDisplayName];
if (isEventSenderMyUser)
{
displayText = [VectorL10n eventFormatterWidgetAddedByYou:(widget.name ? widget.name : widget.type)];
}
else
{
displayText = [VectorL10n eventFormatterWidgetAdded:(widget.name ? widget.name : widget.type) :senderDisplayName];
}
}
}
else
{
if (isEventSenderMyUser)
// This is a closed widget
// Check if it corresponds to a jitsi widget by looking at other state events for
// this jitsi widget (widget id = event.stateKey).
// Get all widgets state events in the room
NSMutableArray<MXEvent*> *widgetStateEvents = [NSMutableArray arrayWithArray:[roomState stateEventsWithType:kWidgetMatrixEventTypeString]];
[widgetStateEvents addObjectsFromArray:[roomState stateEventsWithType:kWidgetModularEventTypeString]];
for (MXEvent *widgetStateEvent in widgetStateEvents)
{
displayText = [VectorL10n eventFormatterWidgetAddedByYou:(widget.name ? widget.name : widget.type)];
}
else
{
displayText = [VectorL10n eventFormatterWidgetAdded:(widget.name ? widget.name : widget.type) :senderDisplayName];
}
}
}
else
{
// This is a closed widget
// Check if it corresponds to a jitsi widget by looking at other state events for
// this jitsi widget (widget id = event.stateKey).
// Get all widgets state events in the room
NSMutableArray<MXEvent*> *widgetStateEvents = [NSMutableArray arrayWithArray:[roomState stateEventsWithType:kWidgetMatrixEventTypeString]];
[widgetStateEvents addObjectsFromArray:[roomState stateEventsWithType:kWidgetModularEventTypeString]];
for (MXEvent *widgetStateEvent in widgetStateEvents)
{
if ([widgetStateEvent.stateKey isEqualToString:widget.widgetId])
{
Widget *activeWidget = [[Widget alloc] initWithWidgetEvent:widgetStateEvent inMatrixSession:mxSession];
if (activeWidget.isActive)
if ([widgetStateEvent.stateKey isEqualToString:widget.widgetId])
{
if ([activeWidget.type isEqualToString:kWidgetTypeJitsiV1]
|| [activeWidget.type isEqualToString:kWidgetTypeJitsiV2])
Widget *activeWidget = [[Widget alloc] initWithWidgetEvent:widgetStateEvent inMatrixSession:mxSession];
if (activeWidget.isActive)
{
// This was a jitsi widget
return nil;
}
else
{
if (isEventSenderMyUser)
if ([activeWidget.type isEqualToString:kWidgetTypeJitsiV1]
|| [activeWidget.type isEqualToString:kWidgetTypeJitsiV2])
{
displayText = [VectorL10n eventFormatterWidgetRemovedByYou:(activeWidget.name ? activeWidget.name : activeWidget.type)];
// This was a jitsi widget
return nil;
}
else
{
displayText = [VectorL10n eventFormatterWidgetRemoved:(activeWidget.name ? activeWidget.name : activeWidget.type) :senderDisplayName];
if (isEventSenderMyUser)
{
displayText = [VectorL10n eventFormatterWidgetRemovedByYou:(activeWidget.name ? activeWidget.name : activeWidget.type)];
}
else
{
displayText = [VectorL10n eventFormatterWidgetRemoved:(activeWidget.name ? activeWidget.name : activeWidget.type) :senderDisplayName];
}
}
break;
}
break;
}
}
}
}
}
if (displayText)
{
if (error)
if (displayText)
{
*error = MXKEventFormatterErrorNone;
}
if (error)
{
*error = MXKEventFormatterErrorNone;
}
// Build the attributed string with the right font and color for the events
return [self renderString:displayText forEvent:event];
// Build the attributed string with the right font and color for the events
return [self renderString:displayText forEvent:event];
}
} else if ([event.type isEqualToString:VoiceBroadcastSettings.eventType]) {
MXLogDebug(@"VB incoming build string")
}
}