diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json new file mode 100644 index 000000000..dbee5479f --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "action_formatting_disabled.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_formatting_disabled@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_formatting_disabled@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png new file mode 100644 index 000000000..f7ef2b190 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png new file mode 100644 index 000000000..270dd75d2 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png new file mode 100644 index 000000000..9394656ff Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/Contents.json new file mode 100644 index 000000000..198b65f6e --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "action_formatting_enabled.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_formatting_enabled@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_formatting_enabled@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled.png new file mode 100644 index 000000000..06798aaa7 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@2x.png new file mode 100644 index 000000000..b2aec071e Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@3x.png new file mode 100644 index 000000000..414aa321f Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@3x.png differ diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/Contents.json new file mode 100644 index 000000000..9a7c05104 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "voice_broadcast_tile_live.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/voice_broadcast_tile_live.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/voice_broadcast_tile_live.svg new file mode 100644 index 000000000..5728fc8d4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/voice_broadcast_tile_live.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/Contents.json new file mode 100644 index 000000000..eeeba86ae --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "voice_broadcast_tile_mic.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/voice_broadcast_tile_mic.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/voice_broadcast_tile_mic.svg new file mode 100644 index 000000000..2f3deca13 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/voice_broadcast_tile_mic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings index 6d9320f4a..9ff00a53d 100644 --- a/Riot/Assets/en.lproj/Untranslated.strings +++ b/Riot/Assets/en.lproj/Untranslated.strings @@ -19,4 +19,3 @@ // MARK: Onboarding Personalization WIP "image_picker_action_files" = "Choose from files"; -"voice_broadcast_in_timeline_title" = "Voice broadcast detected (under active development)"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 24b07b68a..a168812bd 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -797,7 +797,7 @@ Tap the + to start adding people."; "settings_labs_enable_new_session_manager" = "New session manager"; "settings_labs_enable_new_client_info_feature" = "Record the client name, version, and url to recognise sessions more easily in session manager"; "settings_labs_enable_new_app_layout" = "New Application Layout"; -"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor (plain text mode coming soon)"; +"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast (under active development)"; "settings_version" = "Version %@"; @@ -2195,6 +2195,8 @@ Tap the + to start adding people."; "voice_broadcast_blocked_by_someone_else_message" = "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one."; "voice_broadcast_already_in_progress_message" = "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one."; "voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast."; +"voice_broadcast_live" = "Live"; +"voice_broadcast_tile" = "Voice broadcast"; // Mark: - Version check diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 922174427..94352b0be 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -197,6 +197,8 @@ 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 actionFormattingDisabled = ImageAsset(name: "action_formatting_disabled") + internal static let actionFormattingEnabled = ImageAsset(name: "action_formatting_enabled") 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") @@ -343,6 +345,8 @@ internal class Asset: NSObject { 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 voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live") + internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic") internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo") } @objcMembers diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 00519d061..1ff68cf26 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7547,7 +7547,7 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableVoiceBroadcast: String { return VectorL10n.tr("Vector", "settings_labs_enable_voice_broadcast") } - /// Try out the rich text editor (plain text mode coming soon) + /// Try out the rich text editor public static var settingsLabsEnableWysiwygComposer: String { return VectorL10n.tr("Vector", "settings_labs_enable_wysiwyg_composer") } @@ -9087,6 +9087,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastBlockedBySomeoneElseMessage: String { return VectorL10n.tr("Vector", "voice_broadcast_blocked_by_someone_else_message") } + /// Live + public static var voiceBroadcastLive: String { + return VectorL10n.tr("Vector", "voice_broadcast_live") + } /// You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. public static var voiceBroadcastPermissionDeniedMessage: String { return VectorL10n.tr("Vector", "voice_broadcast_permission_denied_message") @@ -9095,6 +9099,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLoadingError: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_loading_error") } + /// Voice broadcast + public static var voiceBroadcastTile: String { + return VectorL10n.tr("Vector", "voice_broadcast_tile") + } /// Can't start a new voice broadcast public static var voiceBroadcastUnauthorizedTitle: String { return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title") diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift index 1f417c770..f273877eb 100644 --- a/Riot/Generated/UntranslatedStrings.swift +++ b/Riot/Generated/UntranslatedStrings.swift @@ -14,10 +14,6 @@ public extension VectorL10n { static var imagePickerActionFiles: String { return VectorL10n.tr("Untranslated", "image_picker_action_files") } - /// Voice broadcast detected (under active development) - static var voiceBroadcastInTimelineTitle: String { - return VectorL10n.tr("Untranslated", "voice_broadcast_in_timeline_title") - } } // swiftlint:enable function_parameter_count identifier_name line_length type_body_length diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index f2024aa88..177fb8b03 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -176,6 +176,9 @@ final class RiotSettings: NSObject { /// Flag indicating if the wysiwyg composer feature is enabled @UserDefault(key: "enableWysiwygComposer", defaultValue: false, storage: defaults) var enableWysiwygComposer + + @UserDefault(key: "enableWysiwygTextFormatting", defaultValue: true, storage: defaults) + var enableWysiwygTextFormatting /// Flag indicating if the IP addresses should be shown in the new device manager @UserDefault(key: UserDefaultsKeys.showIPAddressesInSessionsManager, defaultValue: false, storage: defaults) diff --git a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m index f2f2cd406..1488377f5 100644 --- a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m +++ b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m @@ -152,7 +152,7 @@ UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; // Finalize cell view customization here - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 94f7346aa..8b3a49a5f 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -105,6 +105,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) */ @property(nonatomic) NSInteger componentIndexOfSentMessageTick; +@property(nonatomic, strong) NSString *voiceBroadcastState; + /** Indicate that both the text message layout and any additional content height are no longer valid and should be recomputed before presentation in a bubble cell. This could be due diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index adcd6692e..712604203 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -186,23 +186,45 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { VoiceBroadcastInfo *voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: event.content]; + + // Check if the state event corresponds to the beginning of a voice broadcast if ([VoiceBroadcastInfo isStartedFor:voiceBroadcastInfo.state]) { - // This state event corresponds to the beginning of a voice broadcast - // Check whether this is a local live broadcast to display it with the recorder view or not - // Note: Because of race condition, the voiceBroadcastService may be running without id here (the sync response may be received before - // the success of the event sending), in that case, we will display a recorder view by default to let the user be able to stop a potential record. - if ([event.sender isEqualToString: self.mxSession.myUserId] && - [voiceBroadcastInfo.deviceId isEqualToString:self.mxSession.myDeviceId] && - self.mxSession.voiceBroadcastService != nil && - ([event.eventId isEqualToString: self.mxSession.voiceBroadcastService.voiceBroadcastInfoEventId] || - self.mxSession.voiceBroadcastService.voiceBroadcastInfoEventId == nil)) + // Retrieve the most recent voice broadcast info. + MXEvent *lastVoiceBroadcastInfoEvent = [roomDataSource.roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject; + if (event.originServerTs > lastVoiceBroadcastInfoEvent.originServerTs) { - self.tag = RoomBubbleCellDataTagVoiceBroadcastRecord; + lastVoiceBroadcastInfoEvent = event; + } + + VoiceBroadcastInfo *lastVoiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: lastVoiceBroadcastInfoEvent.content]; + + // Handle the specific case where the state event is a started voice broadcast (the voiceBroadcastId is the event id itself). + if (!lastVoiceBroadcastInfo.voiceBroadcastId) + { + lastVoiceBroadcastInfo.voiceBroadcastId = lastVoiceBroadcastInfoEvent.eventId; + } + + // Check if the voice broadcast is still alive. + if ([lastVoiceBroadcastInfo.voiceBroadcastId isEqualToString:event.eventId] && ![VoiceBroadcastInfo isStoppedFor:lastVoiceBroadcastInfo.state]) + { + // Check whether this broadcast is sent from the currrent session to display it with the recorder view or not. + if ([event.stateKey isEqualToString:self.mxSession.myUserId] && + [voiceBroadcastInfo.deviceId isEqualToString:self.mxSession.myDeviceId]) + { + self.tag = RoomBubbleCellDataTagVoiceBroadcastRecord; + } + else + { + self.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback; + } + + self.voiceBroadcastState = lastVoiceBroadcastInfo.state; } else { self.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback; + self.voiceBroadcastState = VoiceBroadcastInfo.stoppedValue; } } else @@ -213,8 +235,9 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { // This state event corresponds to the end of a voice broadcast // Force the tag of the potential cellData which corresponds to the started event to switch the display from recorder to listener - id bubbleData = [roomDataSource cellDataOfEventWithEventId:voiceBroadcastInfo.eventId]; + RoomBubbleCellData *bubbleData = [roomDataSource cellDataOfEventWithEventId:voiceBroadcastInfo.voiceBroadcastId]; bubbleData.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback; + bubbleData.voiceBroadcastState = VoiceBroadcastInfo.stoppedValue; } } self.collapsable = NO; diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 842623818..5d76bd5f6 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -395,7 +395,7 @@ const CGFloat kTypingCellHeight = 24; id cellDecorator = [RoomTimelineConfiguration shared].currentStyle.cellDecorator; // Finalize cell view customization here - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; [self resetAccessibilityForCell:bubbleCell]; diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 8c1376bce..de6a691f4 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -1859,7 +1859,7 @@ CGFloat localPositionOfEvent = 0.0; - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; @@ -2303,7 +2303,7 @@ CGFloat eventBottomPosition = eventTopPosition + cell.frame.size.height; // Compute accurate event positions in case of bubble with multiple components - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; NSArray *bubbleComponents = roomBubbleTableViewCell.bubbleData.bubbleComponents; @@ -2604,11 +2604,11 @@ roomDataSource.showBubblesDateTime = !roomDataSource.showBubblesDateTime; MXLogDebug(@" -> Turn %@ cells date", roomDataSource.showBubblesDateTime ? @"ON" : @"OFF"); } - else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { [self showAttachmentInCell:(MXKRoomBubbleTableViewCell *)cell]; } - else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnProgressView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnProgressView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; @@ -2719,7 +2719,7 @@ } } } - else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { [self dismissKeyboard]; @@ -3089,7 +3089,7 @@ return; } - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; selectedText = roomBubbleTableViewCell.bubbleData.textMessage; @@ -3628,7 +3628,7 @@ // Keep here the image view used to display the attachment in the selected cell. // Note: Only `MXKRoomBubbleTableViewCell` and `MXKSearchTableViewCell` are supported for the moment. - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { self.openedAttachmentImageView = ((MXKRoomBubbleTableViewCell *)cell).attachmentView.imageView; } @@ -3806,7 +3806,7 @@ }]; - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { // Start animation in case of download MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 8929aeec2..10b002abf 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3237,30 +3237,30 @@ static CGSize kThreadListBarButtonItemImageSize; { if (bubbleData.isPaginationFirstBubble) { - cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle; + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo; + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo; } else { - cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcast; + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback; } } else { if (bubbleData.isPaginationFirstBubble) { - cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle; + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo; + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo; } else { - cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcast; + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback; } } } @@ -5115,7 +5115,9 @@ static CGSize kThreadListBarButtonItemImageSize; [actionItems addObject:@(ComposerCreateActionCamera)]; } - self.composerCreateActionListBridgePresenter = [[ComposerCreateActionListBridgePresenter alloc] initWithActions:actionItems]; + self.composerCreateActionListBridgePresenter = [[ComposerCreateActionListBridgePresenter alloc] initWithActions:actionItems + wysiwygEnabled:RiotSettings.shared.enableWysiwygComposer + textFormattingEnabled:RiotSettings.shared.enableWysiwygTextFormatting]; self.composerCreateActionListBridgePresenter.delegate = self; [self.composerCreateActionListBridgePresenter presentFrom:self animated:YES]; } @@ -5268,7 +5270,7 @@ static CGSize kThreadListBarButtonItemImageSize; } } - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; if (roomBubbleTableViewCell.readMarkerView) @@ -6522,7 +6524,7 @@ static CGSize kThreadListBarButtonItemImageSize; if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking && self.roomDataSource.showReadMarker && self.roomDataSource.room.accountData.readMarkerEventId) { UITableViewCell *cell = [self.bubblesTableView visibleCells].firstObject; - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; // Check whether the read marker is inside the first displayed cell. @@ -8054,6 +8056,11 @@ static CGSize kThreadListBarButtonItemImageSize; }]; } +- (void)composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter enabled:(BOOL)enabled +{ + [self togglePlainTextMode]; +} + - (void)composerCreateActionListBridgePresenterDidDismissInteractively:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter { self.composerCreateActionListBridgePresenter = nil; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 7bbc6812c..1acdf90f8 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -149,6 +149,11 @@ extension RoomViewController { } } } + + @objc func togglePlainTextMode() { + RiotSettings.shared.enableWysiwygTextFormatting.toggle() + wysiwygInputToolbar?.textFormattingEnabled.toggle() + } } // MARK: - Private Helpers diff --git a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m index 9c0530a7b..2790df090 100644 --- a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m +++ b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m @@ -131,7 +131,7 @@ UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; // Finalize cell view customization here - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; diff --git a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h index 3348df0e6..5a91d01e3 100644 --- a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h +++ b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h @@ -170,13 +170,13 @@ typedef NS_ENUM(NSUInteger, RoomTimelineCellIdentifier) { // - Voice broadcast // -- Incoming - RoomTimelineCellIdentifierIncomingVoiceBroadcast, - RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo, - RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle, + RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback, + RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle, // -- Outgoing - RoomTimelineCellIdentifierOutgoingVoiceBroadcast, - RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo, - RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle, + RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback, + RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle, // - Voice broadcast recorder RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder, diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m index c747476ee..4cfb03de1 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m @@ -135,12 +135,12 @@ - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView { // Incoming - [tableView registerClass:VoiceBroadcastIncomingBubbleCell.class forCellReuseIdentifier:VoiceBroadcastIncomingBubbleCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastIncomingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastIncomingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackIncomingBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackIncomingBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; // Outgoing - [tableView registerClass:VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; } - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView @@ -311,17 +311,17 @@ }; } -- (NSDictionary*)voiceBroadcastCellsMapping +- (NSDictionary*)voiceBroadcastPlaybackCellsMapping { return @{ // Incoming - @(RoomTimelineCellIdentifierIncomingVoiceBroadcast) : VoiceBroadcastIncomingBubbleCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastIncomingWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackIncomingBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.class, // Outgoing - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcast) : VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.class, }; } diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingBubbleCell.swift similarity index 92% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingBubbleCell.swift index f46acbae1..fda3dbd6d 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastIncomingBubbleCell: VoiceBroadcastBubbleCell, BubbleIncomingRoomCellProtocol { +class VoiceBroadcastPlaybackIncomingBubbleCell: VoiceBroadcastPlaybackBubbleCell, BubbleIncomingRoomCellProtocol { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.swift similarity index 87% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithPaginationTitleBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.swift index 6bbb10d9a..979ccd27f 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithPaginationTitleBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastIncomingWithPaginationTitleBubbleCell: VoiceBroadcastIncomingBubbleCell { +class VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell: VoiceBroadcastPlaybackIncomingBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithoutSenderInfoPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.swift similarity index 87% rename from Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithoutSenderInfoPlainCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.swift index 6f3ec9110..7a99e2ecb 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithoutSenderInfoPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastWithoutSenderInfoPlainCell: VoiceBroadcastPlainCell { +class VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell: VoiceBroadcastPlaybackIncomingBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.swift similarity index 85% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.swift index 72f69e4d7..34bf80670 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastOutgoingWithPaginationTitleBubbleCell: VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell { +class VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell: VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.swift similarity index 92% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.swift index b149647b6..3616469e9 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell: VoiceBroadcastBubbleCell, BubbleOutgoingRoomCellProtocol { +class VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell: VoiceBroadcastPlaybackBubbleCell, BubbleOutgoingRoomCellProtocol { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackBubbleCell.swift similarity index 96% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackBubbleCell.swift index 67db62e88..2de4341db 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackBubbleCell.swift @@ -16,7 +16,7 @@ import UIKit -class VoiceBroadcastBubbleCell: VoiceBroadcastPlainCell { +class VoiceBroadcastPlaybackBubbleCell: VoiceBroadcastPlaybackPlainCell { // MARK: - Properties @@ -95,7 +95,7 @@ class VoiceBroadcastBubbleCell: VoiceBroadcastPlainCell { } // MARK: - RoomCellTimestampDisplayable -extension VoiceBroadcastBubbleCell: TimestampDisplayable { +extension VoiceBroadcastPlaybackBubbleCell: TimestampDisplayable { func addTimestampView(_ timestampView: UIView) { guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else { diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainBubbleCell.swift new file mode 100644 index 000000000..365a15956 --- /dev/null +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainBubbleCell.swift @@ -0,0 +1,37 @@ +// +// 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 VoiceBroadcastPlaybackPlainBubbleCell: VoiceBroadcastPlaybackBubbleCell { + + override func setupViews() { + super.setupViews() + + // TODO: VB update margins attributes + let leftMargin: CGFloat = BubbleRoomCellLayoutConstants.incomingBubbleBackgroundMargins.left + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left + let rightMargin: CGFloat = 15 + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right + + roomCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin + roomCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin + } + + override func update(theme: Theme) { + super.update(theme: theme) + + self.bubbleBackgroundColor = theme.roomCellIncomingBubbleBackgroundColor + } +} diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainBubbleCell.swift new file mode 100644 index 000000000..69f94e8fa --- /dev/null +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainBubbleCell.swift @@ -0,0 +1,37 @@ +// +// 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 VoiceBroadcastRecorderPlainBubbleCell: VoiceBroadcastRecorderBubbleCell { + + override func setupViews() { + super.setupViews() + + // TODO: VB update margins attributes + let leftMargin: CGFloat = BubbleRoomCellLayoutConstants.incomingBubbleBackgroundMargins.left + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left + let rightMargin: CGFloat = 15 + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right + + roomCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin + roomCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin + } + + override func update(theme: Theme) { + super.update(theme: theme) + + self.bubbleBackgroundColor = theme.roomCellIncomingBubbleBackgroundColor + } +} diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift similarity index 76% rename from Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift index 14c602c4c..8987cb1de 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift @@ -16,19 +16,22 @@ import Foundation -class VoiceBroadcastPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable { +class VoiceBroadcastPlaybackPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable { 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 controller = VoiceBroadcastPlaybackProvider.shared.buildVoiceBroadcastPlaybackVCForEvent(event, senderDisplayName: bubbleData.senderDisplayName) else { + let controller = VoiceBroadcastPlaybackProvider.shared.buildVoiceBroadcastPlaybackVCForEvent(event, + senderDisplayName: bubbleData.senderDisplayName, + voiceBroadcastState: bubbleData.voiceBroadcastState) + else { return } @@ -54,4 +57,4 @@ class VoiceBroadcastPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable } } -extension VoiceBroadcastPlainCell: RoomCellThreadSummaryDisplayable {} +extension VoiceBroadcastPlaybackPlainCell: RoomCellThreadSummaryDisplayable {} diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithPaginationTitlePlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithPaginationTitlePlainCell.swift similarity index 88% rename from Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithPaginationTitlePlainCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithPaginationTitlePlainCell.swift index fa3c3bc50..09f0bcff5 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithPaginationTitlePlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithPaginationTitlePlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastWithPaginationTitlePlainCell: VoiceBroadcastPlainCell { +class VoiceBroadcastPlaybackWithPaginationTitlePlainCell: VoiceBroadcastPlaybackPlainBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.swift similarity index 88% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.swift index 4f123da7d..41f98f81a 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastIncomingWithoutSenderInfoBubbleCell: VoiceBroadcastIncomingBubbleCell { +class VoiceBroadcastPlaybackWithoutSenderInfoPlainCell: VoiceBroadcastPlaybackPlainBubbleCell { override func setupViews() { super.setupViews() 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 index 4247f306c..5c0bb9143 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastRecorderWithPaginationTitlePlainCell: VoiceBroadcastRecorderPlainCell { +class VoiceBroadcastRecorderWithPaginationTitlePlainCell: VoiceBroadcastRecorderPlainBubbleCell { override func setupViews() { super.setupViews() 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 index 172b10aee..797ab8c57 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastRecorderWithoutSenderInfoPlainCell: VoiceBroadcastRecorderPlainCell { +class VoiceBroadcastRecorderWithoutSenderInfoPlainCell: VoiceBroadcastRecorderPlainBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h index b1e85a621..21d8b11bc 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h @@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary*)locationCellsMapping; -- (NSDictionary*)voiceBroadcastCellsMapping; +- (NSDictionary*)voiceBroadcastPlaybackCellsMapping; - (NSDictionary*)voiceBroadcastRecorderCellsMapping; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m index 4813b539d..83b835579 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m @@ -276,14 +276,14 @@ - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView { - [tableView registerClass:VoiceBroadcastPlainCell.class forCellReuseIdentifier:VoiceBroadcastPlainCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastWithoutSenderInfoPlainCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastWithPaginationTitlePlainCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackPlainBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackPlainBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackWithPaginationTitlePlainCell.defaultReuseIdentifier]; } - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView { - [tableView registerClass:VoiceBroadcastRecorderPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderPlainCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastRecorderPlainBubbleCell.class forCellReuseIdentifier:VoiceBroadcastRecorderPlainBubbleCell.defaultReuseIdentifier]; [tableView registerClass:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.defaultReuseIdentifier]; [tableView registerClass:VoiceBroadcastRecorderWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithPaginationTitlePlainCell.defaultReuseIdentifier]; } @@ -346,8 +346,8 @@ NSDictionary *locationCellsMapping = [self locationCellsMapping]; [cellClasses addEntriesFromDictionary:locationCellsMapping]; - NSDictionary *voiceBroadcastCellsMapping = [self voiceBroadcastCellsMapping]; - [cellClasses addEntriesFromDictionary:voiceBroadcastCellsMapping]; + NSDictionary *voiceBroadcastPlaybackCellsMapping = [self voiceBroadcastPlaybackCellsMapping]; + [cellClasses addEntriesFromDictionary:voiceBroadcastPlaybackCellsMapping]; NSDictionary *voiceBroadcastRecorderCellsMapping = [self voiceBroadcastRecorderCellsMapping]; [cellClasses addEntriesFromDictionary:voiceBroadcastRecorderCellsMapping]; @@ -574,17 +574,17 @@ }; } -- (NSDictionary*)voiceBroadcastCellsMapping +- (NSDictionary*)voiceBroadcastPlaybackCellsMapping { return @{ // Incoming - @(RoomTimelineCellIdentifierIncomingVoiceBroadcast) : VoiceBroadcastPlainCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastWithoutSenderInfoPlainCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastWithPaginationTitlePlainCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackPlainBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackWithPaginationTitlePlainCell.class, // Outoing - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcast) : VoiceBroadcastPlainCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastWithoutSenderInfoPlainCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastWithPaginationTitlePlainCell.class + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackPlainBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackWithPaginationTitlePlainCell.class }; } @@ -592,7 +592,7 @@ { return @{ // Outoing - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderPlainCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderPlainBubbleCell.class, @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo) : VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class, @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle) : VoiceBroadcastRecorderWithPaginationTitlePlainCell.class }; diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 368dfaf65..dfc53844b 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -66,6 +66,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp viewModel.callback = { [weak self] result in self?.handleViewModelResult(result) } + wysiwygViewModel.plainTextMode = !RiotSettings.shared.enableWysiwygTextFormatting inputAccessoryViewForKeyboard = UIView(frame: .zero) @@ -99,6 +100,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp subView.trailingAnchor.constraint(equalTo: self.trailingAnchor), subView.bottomAnchor.constraint(equalTo: self.bottomAnchor) ]) + cancellables = [ hostingViewController.heightPublisher .removeDuplicates() @@ -135,7 +137,8 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } private func sendWysiwygMessage(content: WysiwygComposerContent) { - delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.plainText) + let html = content.html.isEmpty ? content.plainText : content.html + delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: html, withRawText: content.plainText) } private func showSendMediaActions() { @@ -212,6 +215,20 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp updatePlaceholderText() } } + + /// Whether text formatting is currently enabled in the composer. + var textFormattingEnabled: Bool { + get { + self.viewModel.textFormattingEnabled + } + set { + self.viewModel.textFormattingEnabled = newValue + self.wysiwygViewModel.plainTextMode = !newValue + if !newValue { + self.wysiwygViewModel.maximised = false + } + } + } /// Add the voice message toolbar to the composer /// - Parameter voiceMessageToolbarView: the voice message toolbar UIView diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index 1de022904..fb90d834d 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -110,7 +110,7 @@ public class VoiceBroadcastAggregator { guard let event = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last, event.stateKey == self.voiceBroadcastSenderId, let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content), - (event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.eventId == self.voiceBroadcastStartEventId), + (event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.voiceBroadcastId == self.voiceBroadcastStartEventId), let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) else { return } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h index 36b963e47..71781d927 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h @@ -32,15 +32,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSInteger chunkLength; /// The event id of the started voice broadcast info state event. -@property (nonatomic, strong, nullable) NSString* eventId; - -/// The event used to build the MXBeaconInfo. -@property (nonatomic, readonly, nullable) MXEvent *originalEvent; +@property (nonatomic, strong, nullable) NSString* voiceBroadcastId; - (instancetype)initWithDeviceId:(NSString *)deviceId state:(NSString *)state chunkLength:(NSInteger)chunkLength - eventId:(NSString *)eventId; + voiceBroadcastId:(NSString *)voiceBroadcastId; @end diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m index 51a50876c..eaaaa9047 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m @@ -22,14 +22,14 @@ - (instancetype)initWithDeviceId:(NSString *)deviceId state:(NSString *)state chunkLength:(NSInteger)chunkLength - eventId:(NSString *)eventId + voiceBroadcastId:(NSString *)voiceBroadcastId { if (self = [super init]) { _deviceId = deviceId; _state = state; _chunkLength = chunkLength; - _eventId = eventId; + _voiceBroadcastId = voiceBroadcastId; } return self; @@ -55,7 +55,7 @@ MXJSONModelSetInteger(chunkLength, JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLength]); } - NSString *eventId; + NSString *voiceBroadcastId; if (JSONDictionary[kMXEventRelationRelatesToKey]) { MXEventContentRelatesTo *relatesTo; @@ -63,11 +63,11 @@ if (relatesTo && [relatesTo.relationType isEqualToString:MXEventRelationTypeReference]) { - eventId = relatesTo.eventId; + voiceBroadcastId = relatesTo.eventId; } } - return [[VoiceBroadcastInfo alloc] initWithDeviceId:deviceId state:state chunkLength:chunkLength eventId:eventId]; + return [[VoiceBroadcastInfo alloc] initWithDeviceId:deviceId state:state chunkLength:chunkLength voiceBroadcastId:voiceBroadcastId]; } - (NSDictionary *)JSONDictionary @@ -78,8 +78,8 @@ JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyState] = self.state; - if (_eventId) { - MXEventContentRelatesTo *relatesTo = [[MXEventContentRelatesTo alloc] initWithRelationType:MXEventRelationTypeReference eventId:_eventId]; + if (_voiceBroadcastId) { + MXEventContentRelatesTo *relatesTo = [[MXEventContentRelatesTo alloc] initWithRelationType:MXEventRelationTypeReference eventId:_voiceBroadcastId]; JSONDictionary[kMXEventRelationRelatesToKey] = relatesTo.JSONDictionary; } else { diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift index 3515a5b59..b2bc1afe4 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift @@ -35,4 +35,20 @@ extension VoiceBroadcastInfo { @objc static func isStopped(for name: String) -> Bool { return name == State.stopped.rawValue } + + @objc static func startedValue() -> String { + return State.started.rawValue + } + + @objc static func pausedValue() -> String { + return State.paused.rawValue + } + + @objc static func resumedValue() -> String { + return State.resumed.rawValue + } + + @objc static func stoppedValue() -> String { + return State.stopped.rawValue + } } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift index 81cbc51af..e6d6171a8 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift @@ -23,15 +23,18 @@ public class VoiceBroadcastService: NSObject { // MARK: - Properties - public private(set) var voiceBroadcastInfoEventId: String? public let room: MXRoom + public private(set) var voiceBroadcastId: String? public private(set) var state: VoiceBroadcastInfo.State + // Mechanism to process one call of sendVoiceBroadcastInfo() at a time + private let asyncTaskQueue: MXAsyncTaskQueue // MARK: - Setup public init(room: MXRoom, state: VoiceBroadcastInfo.State) { self.room = room self.state = state + self.asyncTaskQueue = MXAsyncTaskQueue(label: "VoiceBroadcastServiceQueueEventSerialQueue-" + MXTools.generateSecret()) } // MARK: - Constants @@ -43,14 +46,13 @@ public class VoiceBroadcastService: NSObject { /// 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) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.started) { [weak self] response in + func startVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.started) { [weak self] response in guard let self = self else { return } switch response { case .success((let eventIdResponse)): - self.voiceBroadcastInfoEventId = eventIdResponse + self.voiceBroadcastId = eventIdResponse completion(.success(eventIdResponse)) case .failure(let error): completion(.failure(error)) @@ -61,25 +63,22 @@ public class VoiceBroadcastService: NSObject { /// 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) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.paused, completion: completion) + func pauseVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.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) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.resumed, completion: completion) + func resumeVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.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) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.stopped, completion: completion) + func stopVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.stopped, completion: completion) } func getState() -> String { @@ -104,19 +103,17 @@ public class VoiceBroadcastService: NSObject { func sendChunkOfVoiceBroadcast(audioFileLocalURL: URL, mimeType: String?, duration: UInt, - samples: [Float]?, sequence: UInt, success: @escaping ((String?) -> Void), failure: @escaping ((Error?) -> Void)) { - guard let voiceBroadcastInfoEventId = self.voiceBroadcastInfoEventId else { + guard let voiceBroadcastId = self.voiceBroadcastId else { return failure(VoiceBroadcastServiceError.notStarted) } self.room.sendChunkOfVoiceBroadcast(localURL: audioFileLocalURL, - voiceBroadcastInfoEventId: voiceBroadcastInfoEventId, + voiceBroadcastId: voiceBroadcastId, mimeType: mimeType, duration: duration, - samples: samples, sequence: sequence, success: success, failure: failure) @@ -124,46 +121,71 @@ public class VoiceBroadcastService: NSObject { // MARK: - Private - private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State, completion: @escaping (MXResponse) -> Void) -> MXHTTPOperation? { + private func allowedStates(from state: VoiceBroadcastInfo.State) -> [VoiceBroadcastInfo.State] { + switch state { + case .started: + return [.paused, .stopped] + case .paused: + return [.resumed, .stopped] + case .resumed: + return [.paused, .stopped] + case .stopped: + return [.started] + } + } + + private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State, completion: @escaping (MXResponse) -> Void) { guard let userId = self.room.mxSession.myUserId else { completion(.failure(VoiceBroadcastServiceError.missingUserId)) - return nil + return } - let stateKey = userId - - let voiceBroadcastInfo = VoiceBroadcastInfo() - - voiceBroadcastInfo.deviceId = self.room.mxSession.myDeviceId - - voiceBroadcastInfo.state = state.rawValue - - if state != VoiceBroadcastInfo.State.started { - guard let voiceBroadcastInfoEventId = self.voiceBroadcastInfoEventId else { - completion(.failure(VoiceBroadcastServiceError.notStarted)) - return nil + asyncTaskQueue.async { (taskCompleted) in + guard self.allowedStates(from: self.state).contains(state) else { + MXLog.warning("[VoiceBroadcastService] sendVoiceBroadcastInfo: unexpected state change \(self.state) -> \(state)") + completion(.failure(VoiceBroadcastServiceError.unexpectedState)) + taskCompleted() + return } - voiceBroadcastInfo.eventId = voiceBroadcastInfoEventId - } else { - voiceBroadcastInfo.chunkLength = BuildSettings.voiceBroadcastChunkLength - } - - guard let stateEventContent = voiceBroadcastInfo.jsonDictionary() as? [String: Any] else { - completion(.failure(VoiceBroadcastServiceError.unknown)) - return nil - } - - return self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), - content: stateEventContent, stateKey: stateKey) { [weak self] response in - guard let self = self else { return } + let stateKey = userId - switch response { - case .success(let object): - self.state = state - completion(.success(object)) - case .failure(let error): - completion(.failure(error)) + let voiceBroadcastInfo = VoiceBroadcastInfo() + + voiceBroadcastInfo.deviceId = self.room.mxSession.myDeviceId + + voiceBroadcastInfo.state = state.rawValue + + if state != VoiceBroadcastInfo.State.started { + guard let voiceBroadcastId = self.voiceBroadcastId else { + completion(.failure(VoiceBroadcastServiceError.notStarted)) + taskCompleted() + return + } + + voiceBroadcastInfo.voiceBroadcastId = voiceBroadcastId + } else { + voiceBroadcastInfo.chunkLength = BuildSettings.voiceBroadcastChunkLength + } + + guard let stateEventContent = voiceBroadcastInfo.jsonDictionary() as? [String: Any] else { + completion(.failure(VoiceBroadcastServiceError.unknown)) + taskCompleted() + return + } + + self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), + 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)) + } + taskCompleted() } } } @@ -176,10 +198,8 @@ extension VoiceBroadcastService { /// - 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 + @objc public func startVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.startVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -193,10 +213,8 @@ extension VoiceBroadcastService { /// - 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 + @objc public func pauseVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.pauseVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -210,10 +228,8 @@ extension VoiceBroadcastService { /// - 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 + @objc public func resumeVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.resumeVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -227,10 +243,8 @@ extension VoiceBroadcastService { /// - 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 + @objc public func stopVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.stopVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -247,7 +261,7 @@ 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. + /// - voiceBroadcastId: The event id of the started voice broadcast info state 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] @@ -257,19 +271,15 @@ extension MXRoom { /// - failure: A closure called when the operation fails. /// - Returns: a `MXHTTPOperation` instance. @nonobjc @discardableResult func sendChunkOfVoiceBroadcast(localURL: URL, - voiceBroadcastInfoEventId: String, + voiceBroadcastId: String, mimeType: String?, duration: UInt, - samples: [Float]?, threadId: String? = nil, sequence: UInt, 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 { + eventId: voiceBroadcastId).jsonDictionary() as? [String: Any] else { failure(VoiceBroadcastServiceError.unknown) return nil } @@ -281,7 +291,7 @@ extension MXRoom { VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: sequenceValue], mimeType: mimeType, duration: duration, - samples: boxedSamples, + samples: nil, threadId: threadId, localEcho: nil, success: success, diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift index 55d0820fa..70f3851e0 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift @@ -21,6 +21,7 @@ public enum VoiceBroadcastServiceError: Int, Error { case missingUserId case roomNotFound case notStarted + case unexpectedState case unknown } diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 80efe2f99..2a1fe4879 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -273,7 +273,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; return [self renderString:displayText forEvent:event]; } } else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { - MXLogDebug(@"VB incoming build string") + // do not show voice broadcast info in the timeline + return nil; } } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift index 41b79334d..6d2a08f0f 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift @@ -18,6 +18,7 @@ import Foundation @objc protocol ComposerCreateActionListBridgePresenterDelegate { func composerCreateActionListBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, action: ComposerCreateAction) + func composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, enabled: Bool) func composerCreateActionListBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter) } @@ -34,6 +35,8 @@ final class ComposerCreateActionListBridgePresenter: NSObject { // MARK: Private private let actions: [ComposerCreateAction] + private let wysiwygEnabled: Bool + private let textFormattingEnabled: Bool private var coordinator: ComposerCreateActionListCoordinator? // MARK: Public @@ -42,10 +45,12 @@ final class ComposerCreateActionListBridgePresenter: NSObject { // MARK: - Setup - init(actions: [Int]) { + init(actions: [Int], wysiwygEnabled: Bool, textFormattingEnabled: Bool) { self.actions = actions.compactMap { ComposerCreateAction(rawValue: $0) } + self.wysiwygEnabled = wysiwygEnabled + self.textFormattingEnabled = textFormattingEnabled super.init() } @@ -57,12 +62,16 @@ final class ComposerCreateActionListBridgePresenter: NSObject { // } func present(from viewController: UIViewController, animated: Bool) { - let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions) + let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions, + wysiwygEnabled: wysiwygEnabled, + textFormattingEnabled: textFormattingEnabled) composerCreateActionListCoordinator.callback = { [weak self] action in guard let self = self else { return } switch action { case .done(let composeAction): self.delegate?.composerCreateActionListBridgePresenterDelegateDidComplete(self, action: composeAction) + case .toggleTextFormatting(let enabled): + self.delegate?.composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting(self, enabled: enabled) case .cancel: self.delegate?.composerCreateActionListBridgePresenterDidDismissInteractively(self) } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift index fcc05c1f2..cb52281eb 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift @@ -19,6 +19,7 @@ import SwiftUI /// Actions returned by the coordinator callback enum ComposerCreateActionListCoordinatorAction { case done(ComposerCreateAction) + case toggleTextFormatting(Bool) case cancel } @@ -39,8 +40,11 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta // MARK: - Setup - init(actions: [ComposerCreateAction]) { - viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions)) + init(actions: [ComposerCreateAction], wysiwygEnabled: Bool, textFormattingEnabled: Bool) { + viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState( + actions: actions, + wysiwygEnabled: wysiwygEnabled, + bindings: ComposerCreateActionListBindings(textFormattingEnabled: textFormattingEnabled))) view = ComposerCreateActionList(viewModel: viewModel.context) let hostingVC = VectorHostingController(rootView: view) hostingVC.bottomSheetPreferences = VectorHostingBottomSheetPreferences( @@ -61,6 +65,8 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta switch result { case .done(let action): self.callback?(.done(action)) + case .toggleTextFormatting(let enabled): + self.callback?(.toggleTextFormatting(enabled)) } } } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift index 31d5b9487..cb1a53b88 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift @@ -33,7 +33,10 @@ enum MockComposerCreateActionListScreenState: MockScreenState, CaseIterable { case .fullList: actions = ComposerCreateAction.allCases } - let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions)) + let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState( + actions: actions, + wysiwygEnabled: true, + bindings: ComposerCreateActionListBindings(textFormattingEnabled: true))) return ( [viewModel], diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift index 457cc612a..6c42041b7 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift @@ -21,11 +21,15 @@ import Foundation enum ComposerCreateActionListViewAction { // The user selected an action case selectAction(ComposerCreateAction) + // The user toggled the text formatting action + case toggleTextFormatting(Bool) } enum ComposerCreateActionListViewModelResult: Equatable { // The user selected an action and is done with the screen case done(ComposerCreateAction) + // The user toggled the text formatting setting but might not be done with the screen + case toggleTextFormatting(Bool) } // MARK: View @@ -33,6 +37,13 @@ enum ComposerCreateActionListViewModelResult: Equatable { struct ComposerCreateActionListViewState: BindableState { /// The list of composer create actions to display to the user let actions: [ComposerCreateAction] + let wysiwygEnabled: Bool + + var bindings: ComposerCreateActionListBindings +} + +struct ComposerCreateActionListBindings { + var textFormattingEnabled: Bool } @objc enum ComposerCreateAction: Int { diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift index 33258467b..35532a212 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift @@ -23,7 +23,13 @@ class ComposerCreateActionListTests: XCTestCase { var context: ComposerCreateActionListViewModel.Context! override func setUpWithError() throws { - viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: ComposerCreateAction.allCases)) + viewModel = ComposerCreateActionListViewModel( + initialViewState: ComposerCreateActionListViewState( + actions: ComposerCreateAction.allCases, + wysiwygEnabled: true, + bindings: ComposerCreateActionListBindings(textFormattingEnabled: true) + ) + ) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift index dbc484372..7f2733e2b 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift @@ -22,11 +22,17 @@ struct ComposerCreateActionList: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI + + private var textFormattingIcon: String { + viewModel.textFormattingEnabled + ? Asset.Images.actionFormattingEnabled.name + : Asset.Images.actionFormattingDisabled.name + } // MARK: Public @ObservedObject var viewModel: ComposerCreateActionListViewModel.Context - + var body: some View { VStack { VStack(alignment: .leading) { @@ -48,6 +54,29 @@ struct ComposerCreateActionList: View { .padding(.horizontal, 16) .padding(.vertical, 12) } + if viewModel.viewState.wysiwygEnabled { + SeparatorLine() + HStack(spacing: 16) { + Image(textFormattingIcon) + .renderingMode(.template) + .foregroundColor(theme.colors.accent) + Text(VectorL10n.wysiwygComposerStartActionTextFormatting) + .foregroundColor(theme.colors.primaryContent) + .font(theme.fonts.body) + .accessibilityIdentifier("textFormatting") + Spacer() + Toggle("", isOn: $viewModel.textFormattingEnabled) + .toggleStyle(ComposerToggleActionStyle()) + .labelsHidden() + .onChange(of: viewModel.textFormattingEnabled) { isOn in + viewModel.send(viewAction: .toggleTextFormatting(isOn)) + } + } + .contentShape(Rectangle()) + .padding(.horizontal, 16) + .padding(.vertical, 12) + + } } .padding(.top, 8) Spacer() @@ -63,3 +92,35 @@ struct ComposerCreateActionList_Previews: PreviewProvider { stateRenderer.screenGroup() } } + +struct ComposerToggleActionStyle: ToggleStyle { + @Environment(\.theme) private var theme + + func makeBody(configuration: Configuration) -> some View { + HStack { + Rectangle() + .foregroundColor(.clear) + .frame(width: 50, height: 30, alignment: .center) + .overlay( + Rectangle() + .foregroundColor(configuration.isOn + ? theme.colors.accent.opacity(0.5) + : theme.colors.primaryContent.opacity(0.25)) + .cornerRadius(7) + .padding(.all, 8) + ) + .overlay( + Circle() + .foregroundColor(configuration.isOn + ? theme.colors.accent + : theme.colors.background) + .padding(.all, 3) + .offset(x: configuration.isOn ? 11 : -11, y: 0) + .shadow(radius: configuration.isOn ? 0.0 : 2.0) + .animation(Animation.linear(duration: 0.1)) + + ).cornerRadius(20) + .onTapGesture { configuration.isOn.toggle() } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift index bd063b1b2..93fa9950b 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift @@ -35,6 +35,8 @@ class ComposerCreateActionListViewModel: ComposerCreateActionListViewModelType, switch viewAction { case .selectAction(let action): callback?(.done(action)) + case .toggleTextFormatting(let enabled): + callback?(.toggleTextFormatting(enabled)) } } } diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift index 2e86750b5..38f1cf227 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift @@ -19,6 +19,7 @@ import Foundation struct ComposerViewState: BindableState { var eventSenderDisplayName: String? var sendMode: ComposerSendMode = .send + var textFormattingEnabled = true var placeholder: String? var bindings: ComposerBindings @@ -26,7 +27,7 @@ struct ComposerViewState: BindableState { extension ComposerViewState { var shouldDisplayContext: Bool { - return sendMode == .edit || sendMode == .reply + sendMode == .edit || sendMode == .reply } var contextDescription: String? { diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index cab370558..2ed64b4e5 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -83,70 +83,10 @@ struct Composer: View { var body: some View { VStack(spacing: 8) { - let rect = RoundedRectangle(cornerRadius: cornerRadius) - VStack(spacing: 12) { - if viewModel.viewState.shouldDisplayContext { - HStack { - if let imageName = viewModel.viewState.contextImageName { - Image(imageName) - .foregroundColor(theme.colors.tertiaryContent) - } - if let contextDescription = viewModel.viewState.contextDescription { - Text(contextDescription) - .accessibilityIdentifier("contextDescription") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(theme.colors.secondaryContent) - } - Spacer() - Button { - viewModel.send(viewAction: .cancel) - } label: { - Image(Asset.Images.inputCloseIcon.name) - .foregroundColor(theme.colors.tertiaryContent) - } - .accessibilityIdentifier("cancelButton") - } - .padding(.top, 8) - .padding(.horizontal, horizontalPadding) - } - HStack(alignment: .top, spacing: 0) { - WysiwygComposerView( - focused: $viewModel.focused, - viewModel: wysiwygViewModel - ) - .tintColor(theme.colors.accent) - .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) - .frame(height: wysiwygViewModel.idealHeight) - .onAppear { - wysiwygViewModel.setup() - } - Button { - wysiwygViewModel.maximised.toggle() - } label: { - Image(toggleButtonImageName) - .resizable() - .foregroundColor(theme.colors.tertiaryContent) - .frame(width: 16, height: 16) - } - .accessibilityIdentifier(toggleButtonAcccessibilityIdentifier) - .padding(.leading, 12) - .padding(.trailing, 4) - } - .padding(.horizontal, horizontalPadding) - .padding(.top, topPadding) - .padding(.bottom, verticalPadding) + if viewModel.viewState.textFormattingEnabled { + composerContainer } - .clipShape(rect) - .overlay(rect.stroke(borderColor, lineWidth: 1)) - .animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight) - .padding(.horizontal, horizontalPadding) - .padding(.top, 8) - .onTapGesture { - if viewModel.focused { - viewModel.focused = true - } - } - HStack(spacing: 0) { + HStack(alignment: .bottom, spacing: 0) { Button { showSendMediaActions() } label: { @@ -159,13 +99,21 @@ struct Composer: View { .background(Circle().fill(theme.colors.system)) .padding(.trailing, 8) .accessibilityLabel(VectorL10n.create) - FormattingToolbar(formatItems: formatItems) { type in - wysiwygViewModel.apply(type.action) + if viewModel.viewState.textFormattingEnabled { + FormattingToolbar(formatItems: formatItems) { type in + wysiwygViewModel.apply(type.action) + } + .frame(height: 44) + Spacer() + } else { + composerContainer } - .frame(height: 44) - Spacer() Button { - sendMessageAction(wysiwygViewModel.content) + if wysiwygViewModel.plainTextMode { + sendMessageAction(wysiwygViewModel.plainTextModeContent) + } else { + sendMessageAction(wysiwygViewModel.content) + } wysiwygViewModel.clearContent() } label: { if viewModel.viewState.sendMode == .edit { @@ -190,6 +138,76 @@ struct Composer: View { .padding(.bottom, 4) } } + + private var composerContainer: some View { + let rect = RoundedRectangle(cornerRadius: cornerRadius) + return VStack(spacing: 12) { + if viewModel.viewState.shouldDisplayContext { + HStack { + if let imageName = viewModel.viewState.contextImageName { + Image(imageName) + .foregroundColor(theme.colors.tertiaryContent) + } + if let contextDescription = viewModel.viewState.contextDescription { + Text(contextDescription) + .accessibilityIdentifier("contextDescription") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(theme.colors.secondaryContent) + } + Spacer() + Button { + viewModel.send(viewAction: .cancel) + } label: { + Image(Asset.Images.inputCloseIcon.name) + .foregroundColor(theme.colors.tertiaryContent) + } + .accessibilityIdentifier("cancelButton") + } + .padding(.top, 8) + .padding(.horizontal, horizontalPadding) + } + HStack(alignment: .top, spacing: 0) { + WysiwygComposerView( + focused: $viewModel.focused, + viewModel: wysiwygViewModel + ) + .tintColor(theme.colors.accent) + .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) + .frame(height: wysiwygViewModel.idealHeight) + .onAppear { + if wysiwygViewModel.isContentEmpty { + wysiwygViewModel.setup() + } + } + if viewModel.viewState.textFormattingEnabled { + Button { + wysiwygViewModel.maximised.toggle() + } label: { + Image(toggleButtonImageName) + .resizable() + .foregroundColor(theme.colors.tertiaryContent) + .frame(width: 16, height: 16) + } + .accessibilityIdentifier(toggleButtonAcccessibilityIdentifier) + .padding(.leading, 12) + .padding(.trailing, 4) + } + } + .padding(.horizontal, horizontalPadding) + .padding(.top, topPadding) + .padding(.bottom, verticalPadding) + } + .clipShape(rect) + .overlay(rect.stroke(borderColor, lineWidth: 1)) + .animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight) + .padding(.horizontal, horizontalPadding) + .padding(.top, 8) + .onTapGesture { + if viewModel.focused { + viewModel.focused = true + } + } + } } // MARK: Previews diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift index 5d57e34b3..8ad3ebd27 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift @@ -35,6 +35,15 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol state.sendMode = newValue } } + + var textFormattingEnabled: Bool { + get { + state.textFormattingEnabled + } + set { + state.textFormattingEnabled = newValue + } + } var eventSenderDisplayName: String? { get { diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift index 41005324b..a1674ff4d 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift @@ -20,6 +20,7 @@ protocol ComposerViewModelProtocol { var context: ComposerViewModelType.Context { get } var callback: ((ComposerViewModelResult) -> Void)? { get set } var sendMode: ComposerSendMode { get set } + var textFormattingEnabled: Bool { get set } var eventSenderDisplayName: String? { get set } var placeholder: String? { get set } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index 4184f0d63..ee7b51e4e 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -48,7 +48,7 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { let voiceBroadcastAggregator = try VoiceBroadcastAggregator(session: parameters.session, room: parameters.room, voiceBroadcastStartEventId: parameters.voiceBroadcastStartEvent.eventId, voiceBroadcastState: parameters.voiceBroadcastState) - let details = VoiceBroadcastPlaybackDetails(senderDisplayName: parameters.senderDisplayName) + let details = VoiceBroadcastPlaybackDetails(senderDisplayName: parameters.senderDisplayName, avatarData: parameters.room.avatarData) viewModel = VoiceBroadcastPlaybackViewModel(details: details, mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider, cacheManager: VoiceMessageAttachmentCacheManager.sharedManager, @@ -61,7 +61,9 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { func start() { } func toPresentable() -> UIViewController { - VectorHostingController(rootView: VoiceBroadcastPlaybackView(viewModel: viewModel.context)) + let view = VoiceBroadcastPlaybackView(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + return VectorHostingController(rootView: view) } func canEndVoiceBroadcast() -> Bool { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift index 5167a2364..7ca72c413 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift @@ -26,7 +26,7 @@ class VoiceBroadcastPlaybackProvider { /// Create or retrieve the voiceBroadcast timeline coordinator for this event and return /// a view to be displayed in the timeline - func buildVoiceBroadcastPlaybackVCForEvent(_ event: MXEvent, senderDisplayName: String?) -> UIViewController? { + func buildVoiceBroadcastPlaybackVCForEvent(_ event: MXEvent, senderDisplayName: String?, voiceBroadcastState: String) -> UIViewController? { guard let session = session, let room = session.room(withRoomId: event.roomId) else { return nil } @@ -35,26 +35,10 @@ class VoiceBroadcastPlaybackProvider { return coordinator.toPresentable() } - let dispatchGroup = DispatchGroup() - dispatchGroup.enter() - var voiceBroadcastState = VoiceBroadcastInfo.State.stopped - - room.state { roomState in - if let stateEvent = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last, - stateEvent.stateKey == event.stateKey, - let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: stateEvent.content), - (stateEvent.eventId == event.eventId || voiceBroadcastInfo.eventId == event.eventId), - let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) { - voiceBroadcastState = state - } - - dispatchGroup.leave() - } - let parameters = VoiceBroadcastPlaybackCoordinatorParameters(session: session, room: room, voiceBroadcastStartEvent: event, - voiceBroadcastState: voiceBroadcastState, + voiceBroadcastState: VoiceBroadcastInfo.State(rawValue: voiceBroadcastState) ?? VoiceBroadcastInfo.State.stopped, senderDisplayName: senderDisplayName) guard let coordinator = try? VoiceBroadcastPlaybackCoordinator(parameters: parameters) else { return nil diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index 04ade8a77..a16c83471 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -45,33 +45,49 @@ struct VoiceBroadcastPlaybackView: View { var body: some View { let details = viewModel.viewState.details - VStack(alignment: .center, spacing: 16.0) { + VStack(alignment: .center) { - HStack { - Text(details.senderDisplayName ?? "") - //Text(VectorL10n.voiceBroadcastInTimelineTitle) - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) + HStack (alignment: .top) { + AvatarImage(avatarData: viewModel.viewState.details.avatarData, size: .xSmall) + + VStack(alignment: .leading, spacing: 0) { + Text(details.avatarData.displayName ?? details.avatarData.matrixItemId) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + Label { + Text(details.senderDisplayName ?? details.avatarData.matrixItemId) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileMic.image) + } + Label { + Text(VectorL10n.voiceBroadcastTile) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + } + }.frame(maxWidth: .infinity, alignment: .leading) if viewModel.viewState.broadcastState == .live { Button { viewModel.send(viewAction: .playLive) } label: { - HStack { - Image(uiImage: Asset.Images.voiceBroadcastLive.image) - .renderingMode(.original) - Text("Live") - .font(theme.fonts.bodySB) + Label { + Text(VectorL10n.voiceBroadcastLive) + .font(theme.fonts.caption1SB) .foregroundColor(Color.white) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastLive.image) } - } - .padding(5.0) - .background(RoundedRectangle(cornerRadius: 4, style: .continuous) - .fill(backgroundColor)) + .padding(.horizontal, 5) + .background(RoundedRectangle(cornerRadius: 4, style: .continuous).fill(backgroundColor)) .accessibilityIdentifier("liveButton") } } - + .frame(maxWidth: .infinity, alignment: .leading) + if viewModel.viewState.playbackState == .error { VoiceBroadcastPlaybackErrorView() } else { @@ -101,13 +117,9 @@ struct VoiceBroadcastPlaybackView: View { } .activityIndicator(show: viewModel.viewState.playbackState == .buffering) } - } .padding([.horizontal, .top], 2.0) .padding([.bottom]) - .alert(item: $viewModel.alertInfo) { info in - info.alert - } } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index 09a12b87d..3fed0075f 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -34,6 +34,7 @@ enum VoiceBroadcastPlaybackState { struct VoiceBroadcastPlaybackDetails { let senderDisplayName: String? + let avatarData: AvatarInputProtocol } enum VoiceBroadcastState { @@ -51,12 +52,5 @@ struct VoiceBroadcastPlaybackViewState: BindableState { } struct VoiceBroadcastPlaybackViewStateBindings { - // TODO: Neeeded? - var alertInfo: AlertInfo? -} - -enum VoiceBroadcastPlaybackAlertType { - // TODO: What is it? - case failedClosingVoiceBroadcast } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index 72a15185f..f4fabadb1 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift @@ -42,7 +42,7 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice") + let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .live, playbackState: .stopped, bindings: VoiceBroadcastPlaybackViewStateBindings())) return ( diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift index c13524e13..e5e0afe3c 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift @@ -45,7 +45,7 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { voiceBroadcastRecorderService = VoiceBroadcastRecorderService(session: parameters.session, roomId: parameters.room.matrixItemId) - let details = VoiceBroadcastRecorderDetails(senderDisplayName: parameters.senderDisplayName) + let details = VoiceBroadcastRecorderDetails(senderDisplayName: parameters.senderDisplayName, avatarData: parameters.room.avatarData) let viewModel = VoiceBroadcastRecorderViewModel(details: details, recorderService: voiceBroadcastRecorderService) voiceBroadcastRecorderViewModel = viewModel @@ -56,7 +56,9 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { func start() { } func toPresentable() -> UIViewController { - VectorHostingController(rootView: VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context)) + let view = VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + return VectorHostingController(rootView: view) } func pauseRecording() { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index d75f69830..0ad1fa682 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -33,7 +33,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { private var chunkFile: AVAudioFile! = nil private var chunkFrames: AVAudioFrameCount = 0 - private var chunkFileNumber: Int = 1 + private var chunkFileNumber: Int = 0 // MARK: Public @@ -63,14 +63,16 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { } try? audioEngine.start() + + // Disable the sleep mode during the recording until we are able to handle it + UIApplication.shared.isIdleTimerDisabled = true } func stopRecordingVoiceBroadcast() { MXLog.debug("[VoiceBroadcastRecorderService] Stop recording voice broadcast") audioEngine.stop() audioEngine.inputNode.removeTap(onBus: audioNodeBus) - - resetValues() + UIApplication.shared.isIdleTimerDisabled = false voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in MXLog.debug("[VoiceBroadcastRecorderService] Stopped") @@ -82,25 +84,33 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // Send current chunk if self.chunkFile != nil { - self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) + self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) { + self.tearDownVoiceBroadcastService() + } + } else { + self.tearDownVoiceBroadcastService() } - - self.session.tearDownVoiceBroadcastService() }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to stop voice broadcast", context: error) + // Discard the service on VoiceBroadcastService error. We keep the service in case of other error type + if error as? VoiceBroadcastServiceError != nil { + self.tearDownVoiceBroadcastService() + } }) } func pauseRecordingVoiceBroadcast() { audioEngine.pause() + UIApplication.shared.isIdleTimerDisabled = false voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in guard let self = self else { return } // Send current chunk - self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) - self.chunkFile = nil - + if self.chunkFile != nil { + self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) + self.chunkFile = nil + } }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to pause voice broadcast", context: error) }) @@ -113,7 +123,8 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { guard let self = self else { return } // Update state - self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .started) + self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .resumed) + UIApplication.shared.isIdleTimerDisabled = true }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to resume voice broadcast", context: error) }) @@ -123,7 +134,13 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { /// Reset chunk values. private func resetValues() { chunkFrames = 0 - chunkFileNumber = 1 + chunkFileNumber = 0 + } + + /// Release the service + private func tearDownVoiceBroadcastService() { + resetValues() + session.tearDownVoiceBroadcastService() } /// Write audio buffer to chunk file. @@ -150,6 +167,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // FIXME: Manage error return } + chunkFileNumber += 1 let temporaryFileName = "VoiceBroadcastChunk-\(roomId)-\(chunkFileNumber)" let fileUrl = directory .appendingPathComponent(temporaryFileName) @@ -165,18 +183,20 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { chunkFile = try? AVAudioFile(forWriting: fileUrl, settings: settings) if chunkFile != nil { - chunkFileNumber += 1 chunkFrames = 0 } else { + chunkFileNumber -= 1 stopRecordingVoiceBroadcast() // FIXME: Manage error ? } } /// Send chunk file to the server. - private func sendChunkFile(at url: URL, sequence: Int) { - guard let voiceBroadcastService = voiceBroadcastService else { + private func sendChunkFile(at url: URL, sequence: Int, completion: (() -> Void)? = nil) { + guard voiceBroadcastService != nil else { // FIXME: Manage error + MXLog.debug("[VoiceBroadcastRecorderService] sendChunkFile: service is not available") + completion?() return } @@ -200,21 +220,29 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { } convertAACToM4A(at: url) { [weak self] convertedUrl in - guard let self = self else { return } + guard let self = self else { + completion?() + return + } + + // Delete the source file. + self.deleteRecording(at: url) if let convertedUrl = convertedUrl { dispatchGroup.notify(queue: .main) { self.voiceBroadcastService?.sendChunkOfVoiceBroadcast(audioFileLocalURL: convertedUrl, mimeType: "audio/mp4", duration: UInt(duration * 1000), - samples: nil, sequence: UInt(sequence)) { eventId in MXLog.debug("[VoiceBroadcastRecorderService] Send voice broadcast chunk with success.") - if eventId != nil { - self.deleteRecording(at: url) - } + self.deleteRecording(at: convertedUrl) + completion?() } failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to send voice broadcast chunk.", context: error) + // Do not delete the file to be sent if request failed, the retry flow will need it + // There's no manual mechanism to clean it up afterwards but the tmp folder + // they live in will eventually be deleted by the system + completion?() } } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift index 71fb41cc1..411ce0333 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift @@ -23,6 +23,13 @@ struct VoiceBroadcastRecorderView: View { @Environment(\.theme) private var theme: ThemeSwiftUI + private var backgroundColor: Color { + if viewModel.viewState.recordingState != .paused { + return theme.colors.alert + } + return theme.colors.quarterlyContent + } + // MARK: Public @ObservedObject var viewModel: VoiceBroadcastRecorderViewModel.Context @@ -30,10 +37,35 @@ struct VoiceBroadcastRecorderView: View { var body: some View { let details = viewModel.viewState.details - VStack(alignment: .leading, spacing: 16.0) { - Text(details.senderDisplayName ?? "") - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) + VStack(alignment: .center) { + + HStack(alignment: .top) { + AvatarImage(avatarData: viewModel.viewState.details.avatarData, size: .xSmall) + + VStack(alignment: .leading, spacing: 0) { + Text(details.avatarData.displayName ?? details.avatarData.matrixItemId) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + Label { + Text(VectorL10n.voiceBroadcastTile) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + } + }.frame(maxWidth: .infinity, alignment: .leading) + + Label { + Text(VectorL10n.voiceBroadcastLive) + .font(theme.fonts.caption1SB) + .foregroundColor(Color.white) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastLive.image) + } + .padding(.horizontal, 5) + .background(RoundedRectangle(cornerRadius: 4, style: .continuous).fill(backgroundColor)) + .accessibilityIdentifier("liveButton") + } HStack(alignment: .top, spacing: 16.0) { Button { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift index b88021bfe..7a2566aad 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift @@ -32,6 +32,7 @@ enum VoiceBroadcastRecorderState { struct VoiceBroadcastRecorderDetails { let senderDisplayName: String? + let avatarData: AvatarInputProtocol } struct VoiceBroadcastRecorderViewState: BindableState { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift index baa9488f4..bc915d36a 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift @@ -31,7 +31,7 @@ enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let details = VoiceBroadcastRecorderDetails(senderDisplayName: "") + let details = VoiceBroadcastRecorderDetails(senderDisplayName: "", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, bindings: VoiceBroadcastRecorderViewStateBindings())) return ( diff --git a/changelog.d/6980.change b/changelog.d/6980.change new file mode 100644 index 000000000..88d3df0f9 --- /dev/null +++ b/changelog.d/6980.change @@ -0,0 +1 @@ +Labs: Rich text-editor - Add support for plain text mode