Files
bundesmessenger-ios/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift
T
JanNiklas Grabowski b298dedc22 chore: update from foss 1.11.19 (MESSENGER-6656)
Merge commit 'f823ab9aae70e8d15ed7cc079210dd9bbbb6c8e1' into feature/foss_update_1_11_19

* commit 'f823ab9aae70e8d15ed7cc079210dd9bbbb6c8e1':
  finish version++
  version++
  comments
  update submodule
  remove obsolete tests
  removed unused code
  update submodule
  fix
  Libolm removal
  update license macro
  update license
  Prepare for new sprint

# Conflicts:
#	Config/AppVersion.xcconfig
#	IDETemplateMacros.plist
#	LICENSE
#	README.md
#	Riot/Categories/MXSession+Riot.m
#	Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift
#	Riot/Managers/KeyValueStorage/Extensions/Keychain.swift
#	Riot/Managers/KeyValueStorage/KeyValueStore.swift
#	Riot/Managers/KeyValueStorage/KeychainStore.swift
#	Riot/Managers/KeyValueStorage/MemoryStore.swift
#	Riot/Managers/PushNotification/PushNotificationService.m
#	Riot/Managers/Settings/RiotSettings.swift
#	Riot/Managers/Settings/Shared/RiotSharedSettings.swift
#	Riot/Modules/Analytics/AnalyticsUIElement.swift
#	Riot/Modules/Application/AppCoordinator.swift
#	Riot/Modules/Application/LegacyAppDelegate.h
#	Riot/Modules/Application/LegacyAppDelegate.m
#	Riot/Modules/Authentication/Legacy/AuthenticationViewController.h
#	Riot/Modules/Authentication/Legacy/AuthenticationViewController.m
#	Riot/Modules/Authentication/Legacy/Views/AuthInputsView.h
#	Riot/Modules/Authentication/Legacy/Views/AuthInputsView.m
#	Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
#	Riot/Modules/Common/Recents/RecentsViewController.m
#	Riot/Modules/Common/WebViewController/WebViewViewController.m
#	Riot/Modules/Contacts/Details/ContactDetailsViewController.m
#	Riot/Modules/Contacts/Views/ContactTableViewCell.m
#	Riot/Modules/Favorites/FavouritesViewController.h
#	Riot/Modules/Favorites/FavouritesViewController.m
#	Riot/Modules/GlobalSearch/UnifiedSearchViewController.m
#	Riot/Modules/People/PeopleViewController.h
#	Riot/Modules/People/PeopleViewController.m
#	Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModel.swift
#	Riot/Modules/Room/DataSources/RoomDataSource.m
#	Riot/Modules/Room/Files/RoomFilesViewController.m
#	Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
#	Riot/Modules/Room/Members/RoomParticipantsViewController.m
#	Riot/Modules/Room/RoomViewController.m
#	Riot/Modules/Room/Settings/RoomSettingsViewController.m
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroViewData.swift
#	Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
#	Riot/Modules/Rooms/RoomsViewController.h
#	Riot/Modules/Rooms/ShowDirectory/Cells/Network/DirectoryNetworkTableHeaderFooterView.swift
#	Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCell.swift
#	Riot/Modules/Rooms/ShowDirectory/PublicRoomsDirectoryViewModel.swift
#	Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift
#	Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift
#	Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift
#	Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift
#	Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift
#	Riot/Modules/SetPinCode/PinCodePreferences.swift
#	Riot/Modules/SetPinCode/SetupBiometrics/BiometricsAuthenticationPresenter.swift
#	Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m
#	Riot/Modules/Settings/Security/SecurityViewController.m
#	Riot/Modules/Settings/SettingsViewController.m
#	Riot/Modules/SplitView/SplitViewCoordinator.swift
#	Riot/Modules/SplitView/SplitViewCoordinatorType.swift
#	Riot/Modules/StartChat/StartChatViewController.m
#	Riot/Modules/TabBar/MasterTabBarController.h
#	Riot/Modules/TabBar/MasterTabBarController.m
#	Riot/Utils/EventFormatter.m
#	Riot/Utils/HTMLFormatter.swift
#	Riot/Utils/Tools.m
#	RiotNSE/NotificationService.swift
2024-10-18 15:45:54 +02:00

341 lines
13 KiB
Swift

//
// Copyright 2021-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import UIKit
/// The number of milliseconds in one second.
private let MSEC_PER_SEC: TimeInterval = 1000
@objcMembers
class RoomGroupCallStatusCell: RoomCallBaseCell {
private static var className: String {
return String(describing: self)
}
/// Action identifier used when the user pressed "Join" button for an active call.
/// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call.
static var joinAction: String {
return self.className + ".join"
}
/// Action identifier used when the user pressed "Leave" button for an active call.
/// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call.
static var leaveAction: String {
return self.className + ".leave"
}
/// Action identifier used when the user pressed "Answer" button for an incoming call.
/// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call.
static var answerAction: String {
return self.className + ".answer"
}
/// Action identifier used when the user pressed "Decline" button for an incoming call.
/// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call.
static var declineAction: String {
return self.className + ".decline"
}
private var callDurationString: String = ""
private var isIncoming: Bool = false
private var widgetEvent: MXEvent!
private var widgetId: String!
private var viewState: ViewState = .unknown {
didSet {
updateBottomContentView()
}
}
private enum Constants {
static let secondsToDisplayAnswerDeclineOptions: TimeInterval = 30
}
private enum ViewState {
case unknown
case ringing
case active
case declined
case ended
}
private static var callDurationFormatter: DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.zeroFormattingBehavior = .dropAll
formatter.allowedUnits = [.hour, .minute, .second]
formatter.unitsStyle = .abbreviated
return formatter
}
private func updateBottomContentView() {
bottomContentView = bottomView(for: viewState)
}
private var callTypeIcon: UIImage {
// always return a video call icon
return Asset.Images.callVideoIcon.image
}
private var isJoined: Bool {
#if canImport(JitsiMeetSDK)
return widgetId != nil &&
AppDelegate.theDelegate().callPresenter.jitsiVC?.widget.widgetId == widgetId
#else
return false
#endif
}
private var actionUserInfo: [AnyHashable: Any]? {
if let event = widgetEvent {
return [kMXKRoomBubbleCellEventKey: event]
}
return nil
}
private func bottomView(for state: ViewState) -> UIView? {
switch state {
case .unknown:
return nil
case .ringing:
let view = HorizontalButtonsContainerView.loadFromNib()
view.firstButton.style = .negative
view.firstButton.setTitle(VectorL10n.eventFormatterCallDecline, for: .normal)
view.firstButton.setImage(Asset.Images.voiceCallHangupIcon.image, for: .normal)
view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
view.firstButton.addTarget(self, action: #selector(declineCallAction(_:)), for: .touchUpInside)
view.secondButton.style = .positive
view.secondButton.setTitle(VectorL10n.eventFormatterCallAnswer, for: .normal)
view.secondButton.setImage(callTypeIcon, for: .normal)
view.secondButton.removeTarget(nil, action: nil, for: .touchUpInside)
view.secondButton.addTarget(self, action: #selector(answerCallAction(_:)), for: .touchUpInside)
return view
case .active:
let view = HorizontalButtonsContainerView.loadFromNib()
view.secondButton.isHidden = true
if isJoined {
// show a "Leave" button
view.firstButton.style = .negative
view.firstButton.setTitle(VectorL10n.eventFormatterGroupCallLeave, for: .normal)
view.firstButton.setImage(nil, for: .normal)
view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
view.firstButton.addTarget(self, action: #selector(leaveAction(_:)), for: .touchUpInside)
} else {
// show a "Join" button
view.firstButton.style = .positive
view.firstButton.setTitle(VectorL10n.eventFormatterGroupCallJoin, for: .normal)
view.firstButton.setImage(callTypeIcon, for: .normal)
view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
view.firstButton.addTarget(self, action: #selector(joinAction(_:)), for: .touchUpInside)
}
return view
case .declined:
let view = HorizontalButtonsContainerView.loadFromNib()
view.secondButton.isHidden = true
view.firstButton.style = .positive
view.firstButton.setTitle(VectorL10n.eventFormatterGroupCallJoin, for: .normal)
view.firstButton.setImage(callTypeIcon, for: .normal)
view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
view.firstButton.addTarget(self, action: #selector(joinAction(_:)), for: .touchUpInside)
return view
case .ended:
return nil
}
}
private func updateStatusTextForEndedCall() {
if callDurationString.count > 0 {
statusText = VectorL10n.eventFormatterCallHasEndedWithTime(callDurationString)
} else {
statusText = VectorL10n.eventFormatterCallHasEnded
}
}
// MARK: - Actions
@objc
private func joinAction(_ sender: CallTileActionButton) {
self.delegate?.cell(self,
didRecognizeAction: Self.joinAction,
userInfo: actionUserInfo)
}
@objc
private func leaveAction(_ sender: CallTileActionButton) {
self.delegate?.cell(self,
didRecognizeAction: Self.leaveAction,
userInfo: actionUserInfo)
}
@objc
private func declineCallAction(_ sender: CallTileActionButton) {
self.delegate?.cell(self,
didRecognizeAction: Self.declineAction,
userInfo: actionUserInfo)
}
@objc
private func answerCallAction(_ sender: CallTileActionButton) {
self.delegate?.cell(self,
didRecognizeAction: Self.answerAction,
userInfo: actionUserInfo)
}
// MARK: - MXKCellRendering
override func render(_ cellData: MXKCellData!) {
super.render(cellData)
viewState = .unknown
guard let bubbleCellData = cellData as? RoomBubbleCellData else {
return
}
let events = bubbleCellData.allLinkedEvents()
MXLog.debug("[RoomGroupCallStatusBubbleCell] render: \(events.count) events: \(events)")
guard let widgetEvent = events
.first(where: {
$0.eventType == .custom &&
($0.type == kWidgetMatrixEventTypeString || $0.type == kWidgetModularEventTypeString)
}) else {
return
}
guard let widgetId = widgetEvent.stateKey else {
return
}
guard let room = bubbleCellData.mxSession.room(withRoomId: widgetEvent.roomId) else {
return
}
callDurationString = readableCallDuration(from: widgetEvent, endEvent: nil)
isIncoming = widgetEvent.sender != bubbleCellData.mxSession.myUserId
self.widgetEvent = widgetEvent
self.widgetId = widgetId
innerContentView.callIconView.image = Asset.Images.callVideoIcon.image
if isIncoming && !isJoined &&
TimeInterval(widgetEvent.age)/MSEC_PER_SEC < Constants.secondsToDisplayAnswerDeclineOptions {
#if canImport(JitsiMeetSDK)
if JitsiService.shared.isWidgetDeclined(withId: widgetId) {
innerContentView.callerNameLabel.text = room.summary.displayName
room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView)
viewState = .declined
statusText = VectorL10n.eventFormatterCallYouDeclined
} else {
innerContentView.callerNameLabel.text = VectorL10n.eventFormatterGroupCallIncoming(bubbleCellData.senderDisplayName, room.summary.displayName)
innerContentView.avatarImageView.setImageURI(bubbleCellData.senderAvatarUrl,
withType: nil,
andImageOrientation: .up,
toFitViewSize: innerContentView.avatarImageView.frame.size,
with: MXThumbnailingMethodCrop,
previewImage: bubbleCellData.senderAvatarPlaceholder,
mediaManager: bubbleCellData.mxSession.mediaManager)
viewState = .ringing
statusText = nil
}
#endif
} else {
innerContentView.callerNameLabel.text = room.summary.displayName
room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView)
}
innerContentView.avatarImageView.defaultBackgroundColor = .clear
room.state { [weak self] (roomState) in
guard let self = self else { return }
guard let widgets = WidgetManager.shared()?.widgets(ofTypes: [
kWidgetTypeJitsiV1,
kWidgetTypeJitsiV2
],
in: room,
with: roomState) else {
self.viewState = .ended
self.updateStatusTextForEndedCall()
return
}
let removeWidgetEvent = roomState?.stateEvents
.filter({ $0.stateKey == widgetId })
.first(where: { $0.content.isEmpty })
self.callDurationString = self.readableCallDuration(from: widgetEvent,
endEvent: removeWidgetEvent)
guard let widget = widgets.first(where: { $0.widgetId == widgetId }) else {
self.viewState = .ended
self.updateStatusTextForEndedCall()
return
}
if widget.isActive {
if !self.isIncoming {
self.viewState = .active
self.statusText = VectorL10n.eventFormatterCallActiveVideo
} else if !self.isJoined &&
TimeInterval(widgetEvent.age)/MSEC_PER_SEC < Constants.secondsToDisplayAnswerDeclineOptions {
#if canImport(JitsiMeetSDK)
if JitsiService.shared.isWidgetDeclined(withId: widgetId) {
self.viewState = .declined
self.statusText = VectorL10n.eventFormatterCallYouDeclined
} else {
self.viewState = .ringing
self.statusText = nil
}
#endif
} else {
self.viewState = .active
self.statusText = VectorL10n.eventFormatterCallActiveVideo
}
} else {
self.viewState = .ended
self.updateStatusTextForEndedCall()
}
}
}
private func callDuration(from startEvent: MXEvent?, endEvent: MXEvent?) -> TimeInterval {
guard let startDate = startEvent?.originServerTs else {
// never started
return 0
}
guard let endDate = endEvent?.originServerTs else {
// not ended yet, compute the diff from now
return (NSTimeIntervalSince1970 - TimeInterval(startDate))/MSEC_PER_SEC
}
// ended, compute the diff between two dates
return TimeInterval(max(0, Double(endDate) - Double(startDate)))/MSEC_PER_SEC
}
private func readableCallDuration(from startEvent: MXEvent?, endEvent: MXEvent?) -> String {
let duration = callDuration(from: startEvent, endEvent: endEvent)
if duration <= 0 {
return ""
}
return RoomGroupCallStatusCell.callDurationFormatter.string(from: duration) ?? ""
}
}