Skip to content

Commit

Permalink
Merge pull request #1961 from nextcloud/feat/noid/unify-call-button-i…
Browse files Browse the repository at this point in the history
…n-chat-view

feat(call): Unify call buttons in chat view
  • Loading branch information
SystemKeeper authored Jan 31, 2025
2 parents a089b27 + 3a9b8e6 commit c0bae96
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 57 deletions.
118 changes: 77 additions & 41 deletions NextcloudTalk/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import SwiftyAttributes
private var generateSummaryFromMessageId: Int?
private var generateSummaryTimer: Timer?

private var startCallSilently: Bool = false

private lazy var unreadMessagesSeparator: NCChatMessage = {
let message = NCChatMessage()

Expand All @@ -57,29 +59,76 @@ import SwiftyAttributes

// MARK: - Call buttons in NavigationBar

func getBarButton(forVideo video: Bool) -> BarButtonItemWithActivity {
func getCallOptionsBarButton() -> BarButtonItemWithActivity {
let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20)
let buttonImage = UIImage(systemName: video ? "video" : "phone", withConfiguration: symbolConfiguration)

let buttonImage = UIImage(systemName: "phone", withConfiguration: symbolConfiguration)
let button = BarButtonItemWithActivity(width: 50, with: buttonImage)
button.innerButton.addAction { [unowned self] in
button.showIndicator()
startCall(withVideo: video, silently: false, button: button)

setupCallOptionsBarButtonMenu(button: button)

return button
}

func setupCallOptionsBarButtonMenu(button: BarButtonItemWithActivity) {
let audioCallAction = UIAction(title: NSLocalizedString("Start call", comment: ""),
subtitle: NSLocalizedString("Only audio and screen shares", comment: ""),
image: UIImage(systemName: "phone")) { [unowned self] _ in
startCall(withVideo: false, silently: startCallSilently, button: button)
}

audioCallAction.accessibilityIdentifier = "Voice only call"
audioCallAction.accessibilityHint = NSLocalizedString("Double tap to start a voice only call", comment: "")

let videoCallAction = UIAction(title: NSLocalizedString("Start video call", comment: ""),
subtitle: NSLocalizedString("Audio, video and screen shares", comment: ""),
image: UIImage(systemName: "video")) { [unowned self] _ in
startCall(withVideo: true, silently: startCallSilently, button: button)
}

videoCallAction.accessibilityIdentifier = "Video call"
videoCallAction.accessibilityHint = NSLocalizedString("Double tap to start a video call", comment: "")

if self.room.hasCall {
audioCallAction.title = NSLocalizedString("Join call", comment: "")
videoCallAction.title = NSLocalizedString("Join video call", comment: "")
} else if self.startCallSilently {
audioCallAction.title = NSLocalizedString("Start call silently", comment: "")
videoCallAction.title = NSLocalizedString("Start video call silently", comment: "")
}

if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilitySilentCall, for: self.room) {
let silentCall = UIAction(title: NSLocalizedString("Call without notification", comment: ""), image: UIImage(systemName: "bell.slash")) { [unowned self] _ in
button.showIndicator()
startCall(withVideo: video, silently: true, button: button)
var callOptions: [UIMenuElement] = [audioCallAction, videoCallAction]

// Only show silent call option when starting a call (not when joining)
if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilitySilentCall, for: self.room), !room.hasCall {
var silentImage = UIImage(systemName: "bell.slash")

if startCallSilently {
silentImage = UIImage(systemName: "bell.slash.fill")?.withTintColor(.systemRed, renderingMode: .alwaysOriginal)
}

let silentCallAction = UIAction(title: NSLocalizedString("Call without notification", comment: ""),
image: silentImage) { [unowned self] _ in
startCallSilently.toggle()
setupCallOptionsBarButtonMenu(button: button)
}

button.innerButton.menu = UIMenu(children: [silentCall])
if #available(iOS 16.0, *) {
silentCallAction.attributes = [.keepsMenuPresented]
}

silentCallAction.accessibilityIdentifier = "Call without notification"
silentCallAction.accessibilityHint = NSLocalizedString("Double tap to enable or disable 'Call without notification' option", comment: "")

let silentMenu = UIMenu(title: "", options: [.displayInline], children: [silentCallAction])
callOptions.append(silentMenu)
}

return button
button.innerButton.menu = UIMenu(title: "", children: callOptions)
button.innerButton.showsMenuAsPrimaryAction = true
}

func startCall(withVideo video: Bool, silently: Bool, button: BarButtonItemWithActivity) {
button.showIndicator()
if self.room.recordingConsent {
let alert = UIAlertController(title: "⚠️" + NSLocalizedString("The call might be recorded", comment: ""),
message: NSLocalizedString("The recording might include your voice, video from camera, and screen share. Your consent is required before joining the call.", comment: ""),
Expand All @@ -100,22 +149,13 @@ import SwiftyAttributes
}
}

private lazy var videoCallButton: BarButtonItemWithActivity = {
let videoCallButton = self.getBarButton(forVideo: true)

videoCallButton.accessibilityLabel = NSLocalizedString("Video call", comment: "")
videoCallButton.accessibilityHint = NSLocalizedString("Double tap to start a video call", comment: "")

return videoCallButton
}()

private lazy var voiceCallButton: BarButtonItemWithActivity = {
let voiceCallButton = self.getBarButton(forVideo: false)
private lazy var callOptionsButton: BarButtonItemWithActivity = {
let callOptionsButton = self.getCallOptionsBarButton()

voiceCallButton.accessibilityLabel = NSLocalizedString("Voice call", comment: "")
voiceCallButton.accessibilityHint = NSLocalizedString("Double tap to start a voice call", comment: "")
callOptionsButton.accessibilityLabel = NSLocalizedString("Call options", comment: "")
callOptionsButton.accessibilityHint = NSLocalizedString("Double tap to display call options", comment: "")

return voiceCallButton
return callOptionsButton
}()

private var messageExpirationTimer: Timer?
Expand Down Expand Up @@ -170,7 +210,7 @@ import SwiftyAttributes
super.viewDidLoad()

if room.supportsCalling {
self.navigationItem.rightBarButtonItems = [videoCallButton, voiceCallButton]
self.navigationItem.rightBarButtonItems = [callOptionsButton]
}

// No sharing options in federation v1
Expand Down Expand Up @@ -226,8 +266,7 @@ import SwiftyAttributes
self.leaveChat()
}

self.videoCallButton.hideIndicator()
self.voiceCallButton.hideIndicator()
self.callOptionsButton.hideIndicator()
}

required init?(coder decoder: NSCoder) {
Expand Down Expand Up @@ -301,10 +340,8 @@ import SwiftyAttributes
func disableRoomControls() {
self.titleView?.isUserInteractionEnabled = false

self.videoCallButton.hideIndicator()
self.videoCallButton.isEnabled = false
self.voiceCallButton.hideIndicator()
self.voiceCallButton.isEnabled = false
self.callOptionsButton.hideIndicator()
self.callOptionsButton.isEnabled = false

self.rightButton.isEnabled = false
self.leftButton.isEnabled = false
Expand All @@ -314,8 +351,7 @@ import SwiftyAttributes
if hasJoinedRoom, !offlineMode {
// Enable room info and call buttons when we joined a room
self.titleView?.isUserInteractionEnabled = true
self.videoCallButton.isEnabled = true
self.voiceCallButton.isEnabled = true
self.callOptionsButton.isEnabled = true
}

// Files/objects can only be send when we're not offline
Expand All @@ -327,17 +363,15 @@ import SwiftyAttributes

if !room.userCanStartCall, !room.hasCall {
// Disable call buttons
self.videoCallButton.isEnabled = false
self.voiceCallButton.isEnabled = false
self.callOptionsButton.isEnabled = false
}

if room.readOnlyState == .readOnly || self.shouldPresentLobbyView() {
// Hide text input
self.setTextInputbarHidden(true, animated: self.isVisible)

// Disable call buttons
self.videoCallButton.isEnabled = false
self.voiceCallButton.isEnabled = false
self.callOptionsButton.isEnabled = false
} else if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room), !room.permissions.contains(.chat) {
// Hide text input
self.setTextInputbarHidden(true, animated: isVisible)
Expand All @@ -353,6 +387,9 @@ import SwiftyAttributes
self.setChatMessage(self.textInputbar.textView.text)
}

// Rebuild the call menu to reflect the current call state
self.setupCallOptionsBarButtonMenu(button: self.callOptionsButton)

if self.presentedInCall {
// Create a close button and remove the call buttons
let barButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
Expand Down Expand Up @@ -797,8 +834,7 @@ import SwiftyAttributes
}

DispatchQueue.main.async {
self.videoCallButton.hideIndicator()
self.voiceCallButton.hideIndicator()
self.callOptionsButton.hideIndicator()
}
}

Expand Down
41 changes: 34 additions & 7 deletions NextcloudTalk/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@
/* No comment provided by engineer. */
"Audio options" = "Audio options";

/* No comment provided by engineer. */
"Audio, video and screen shares" = "Audio, video and screen shares";

/* No comment provided by engineer. */
"Audios" = "Audios";

Expand Down Expand Up @@ -394,6 +397,9 @@
/* No comment provided by engineer. */
"Call notification sent" = "Call notification sent";

/* No comment provided by engineer. */
"Call options" = "Call options";

/* No comment provided by engineer. */
"Call recording enabled?" = "Call recording enabled?";

Expand Down Expand Up @@ -799,9 +805,15 @@
/* No comment provided by engineer. */
"Double tap to dismiss sharing options" = "Double tap to dismiss sharing options";

/* No comment provided by engineer. */
"Double tap to display call options" = "Double tap to display call options";

/* No comment provided by engineer. */
"Double tap to edit profile" = "Double tap to edit profile";

/* No comment provided by engineer. */
"Double tap to enable or disable 'Call without notification' option" = "Double tap to enable or disable 'Call without notification' option";

/* No comment provided by engineer. */
"Double tap to enable or disable the camera" = "Double tap to enable or disable the camera";

Expand Down Expand Up @@ -848,7 +860,7 @@
"Double tap to start a video call" = "Double tap to start a video call";

/* No comment provided by engineer. */
"Double tap to start a voice call" = "Double tap to start a voice call";
"Double tap to start a voice only call" = "Double tap to start a voice only call";

/* No comment provided by engineer. */
"Double tap to stop recording" = "Double tap to stop recording";
Expand Down Expand Up @@ -1069,6 +1081,9 @@
/* No comment provided by engineer. */
"Join a conversation or start a new one" = "Join a conversation or start a new one";

/* No comment provided by engineer. */
"Join call" = "Join call";

/* No comment provided by engineer. */
"Join call (audio only)" = "Join call (audio only)";

Expand All @@ -1078,6 +1093,9 @@
/* No comment provided by engineer. */
"Join open conversations" = "Join open conversations";

/* No comment provided by engineer. */
"Join video call" = "Join video call";

/* No comment provided by engineer. */
"Last day (inclusive)" = "Last day (inclusive)";

Expand Down Expand Up @@ -1378,6 +1396,9 @@
/* No comment provided by engineer. */
"Online status" = "Online status";

/* No comment provided by engineer. */
"Only audio and screen shares" = "Only audio and screen shares";

/* No comment provided by engineer. */
"Only normal chat messages can be deleted" = "Only normal chat messages can be deleted";

Expand Down Expand Up @@ -1762,12 +1783,24 @@
/* No comment provided by engineer. */
"Speech recognition failed" = "Speech recognition failed";

/* No comment provided by engineer. */
"Start call" = "Start call";

/* No comment provided by engineer. */
"Start call silently" = "Start call silently";

/* No comment provided by engineer. */
"Start recording" = "Start recording";

/* No comment provided by engineer. */
"Start time" = "Start time";

/* No comment provided by engineer. */
"Start video call" = "Start video call";

/* No comment provided by engineer. */
"Start video call silently" = "Start video call silently";

/* No comment provided by engineer. */
"Status" = "Status";

Expand Down Expand Up @@ -1993,18 +2026,12 @@
/* No comment provided by engineer. */
"Version" = "Version";

/* No comment provided by engineer. */
"Video call" = "Video call";

/* No comment provided by engineer. */
"Video quality" = "Video quality";

/* Conversation visibility settings */
"Visibility" = "Visibility";

/* No comment provided by engineer. */
"Voice call" = "Voice call";

/* No comment provided by engineer. */
"Voice messages" = "Voice messages";

Expand Down
21 changes: 12 additions & 9 deletions NextcloudTalkTests/UI/UIRoomTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ final class UIRoomTest: XCTestCase {
XCTAssert(chatTitleView.waitForExistence(timeout: TestConstants.timeoutShort))

// Wait until we joined the room and the call buttons get active
let voiceCallButton = chatNavBar.buttons["Voice call"]
XCTAssert(voiceCallButton.waitForExistence(timeout: TestConstants.timeoutShort))
waitForEnabled(object: voiceCallButton)
waitForHittable(object: voiceCallButton)
let callOptionsButton = chatNavBar.buttons["Call options"]
XCTAssert(callOptionsButton.waitForExistence(timeout: TestConstants.timeoutShort))
waitForEnabled(object: callOptionsButton)
waitForHittable(object: callOptionsButton)

// Open conversation settings
chatTitleView.tap()
Expand Down Expand Up @@ -91,11 +91,14 @@ final class UIRoomTest: XCTestCase {

// Start a call
let chatNavBar = app.navigationBars["NextcloudTalk.ChatView"]
let voiceCallButton = chatNavBar.buttons["Voice call"]
XCTAssert(voiceCallButton.waitForExistence(timeout: TestConstants.timeoutShort))
waitForEnabled(object: voiceCallButton)
waitForHittable(object: voiceCallButton)
let callOptionsButton = chatNavBar.buttons["Call options"]
XCTAssert(callOptionsButton.waitForExistence(timeout: TestConstants.timeoutShort))
waitForEnabled(object: callOptionsButton)
waitForHittable(object: callOptionsButton)
callOptionsButton.tap()

let voiceCallButton = app.buttons["Voice only call"]
XCTAssert(voiceCallButton.waitForExistence(timeout: TestConstants.timeoutShort))
voiceCallButton.tap()

let hangupCallButton = app.buttons["Hang up"]
Expand All @@ -105,7 +108,7 @@ final class UIRoomTest: XCTestCase {
hangupCallButton.tap()

// Go back to the main view controller
XCTAssert(voiceCallButton.waitForExistence(timeout: TestConstants.timeoutShort))
XCTAssert(callOptionsButton.waitForExistence(timeout: TestConstants.timeoutShort))
chatNavBar.buttons["Back"].tap()

// Check if all chat view controllers are deallocated
Expand Down

0 comments on commit c0bae96

Please sign in to comment.