diff --git a/Signal/src/Calls/CallService.swift b/Signal/src/Calls/CallService.swift index dac4093d46..fb26f8087c 100644 --- a/Signal/src/Calls/CallService.swift +++ b/Signal/src/Calls/CallService.swift @@ -7,6 +7,7 @@ import SignalRingRTC import PromiseKit // All Observer methods will be invoked from the main thread. +@objc(OWSCallServiceObserver) protocol CallServiceObserver: class { /** * Fired whenever the call changes. @@ -138,6 +139,7 @@ public final class CallService: NSObject { private var observers = WeakArray() // The observer-related methods should be invoked on the main thread. + @objc func addObserverAndSyncState(observer: CallServiceObserver) { AssertIsOnMainThread() @@ -148,6 +150,7 @@ public final class CallService: NSObject { } // The observer-related methods should be invoked on the main thread. + @objc func removeObserver(_ observer: CallServiceObserver) { AssertIsOnMainThread() observers.removeAll { $0 === observer } diff --git a/Signal/src/Calls/SignalCall.swift b/Signal/src/Calls/SignalCall.swift index fb7f0a2702..e792ddc812 100644 --- a/Signal/src/Calls/SignalCall.swift +++ b/Signal/src/Calls/SignalCall.swift @@ -92,6 +92,7 @@ public class SignalCall: NSObject, CallManagerCallReference { didSet { AssertIsOnMainThread() } } + @objc public let thread: TSThread? public var error: CallError? diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 2043c1a88c..f55cfac8be 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -44,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)conversationCell:(ConversationViewCell *)cell didEndLongpress:(id)viewItem; - (void)conversationCell:(ConversationViewCell *)cell didTapReactions:(id)viewItem; - (BOOL)conversationCellHasPendingMessageRequest:(ConversationViewCell *)cell; +@property (nonatomic, readonly) BOOL isCallingSupported; #pragma mark - Selection diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m index bb93a259d0..b1db02421f 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m @@ -791,6 +791,7 @@ typedef void (^SystemMessageActionBlock)(void); case RPRecentCallTypeIncomingAnsweredElsewhere: case RPRecentCallTypeIncomingDeclinedElsewhere: case RPRecentCallTypeIncomingBusyElsewhere: + // If delegate is nil, include the action. This way we always oversize instead of undersize. if ([self.delegate conversationCellHasPendingMessageRequest:self]) { return nil; } @@ -800,6 +801,7 @@ typedef void (^SystemMessageActionBlock)(void); accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"call_back")]; case RPRecentCallTypeOutgoing: case RPRecentCallTypeOutgoingMissed: + // If delegate is nil, include the action. This way we always oversize instead of undersize. if ([self.delegate conversationCellHasPendingMessageRequest:self]) { return nil; } @@ -815,21 +817,20 @@ typedef void (^SystemMessageActionBlock)(void); - (nullable SystemMessageAction *)actionForGroupCall:(OWSGroupCallMessage *)groupCallMessage { - // TODO: Check if the current call belongs to this thread. - if (self.callService.currentCall != nil) { - return nil; - } - + // Assume the current thread supports calling if we have no delegate. This ensures we always + // overestimate cell measurement in cases where the current thread doesn't support calling. + BOOL isCallingSupported = !self.delegate || self.delegate.isCallingSupported; BOOL isCallActive = (!groupCallMessage.hasEnded && groupCallMessage.joinedMemberAddresses.count > 0); - // TODO: Respect -canCall from ConversationViewController - // TODO: "Return to call" if for the current thread. Hide the action is currently in a call - // For now we'll just check the feature flag - BOOL canCall = SSKFeatureFlags.groupCalling; + if (isCallingSupported && isCallActive) { + NSString *currentCallThreadId = self.callService.currentCall.thread.uniqueId; + BOOL isCurrentCallForThread = [currentCallThreadId isEqualToString:self.viewItem.thread.uniqueId]; + + NSString *joinTitle = NSLocalizedString(@"GROUP_CALL_JOIN_BUTTON", "Button to join an ongoing group call"); + NSString *returnTitle = NSLocalizedString(@"CALL_RETURN_BUTTON", "Button to return to the current call"); + NSString *actionTitle = isCurrentCallForThread ? returnTitle : joinTitle; - if (isCallActive && canCall) { __weak typeof(self) wSelf = self; - NSString *actionTitle = NSLocalizedString(@"GROUP_CALL_JOIN_BUTTON", "Button to join an ongoing group call"); return [SystemMessageAction actionWithTitle:actionTitle block:^{ [wSelf.delegate handleGroupCallTap]; } accessibilityIdentifier:actionTitle]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 5a20c6e8c2..9bb7216c0a 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1935,7 +1935,11 @@ typedef enum : NSUInteger { - (void)handleGroupCallTap { - [self showGroupCallLobby]; + if ([self.callService.currentCall.thread.uniqueId isEqualToString:self.thread.uniqueId]) { + // TODO + } else { + [self showGroupCallLobby]; + } } - (void)updateSystemContactWithAddress:(SignalServiceAddress *)address @@ -2281,6 +2285,11 @@ typedef enum : NSUInteger { return self.threadViewModel.hasPendingMessageRequest; } +- (BOOL)isCallingSupported +{ + return [self canCall]; +} + - (BOOL)isShowingSelectionUI { return self.uiMode == ConversationUIMode_Selection; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m index c14a822588..3dd2f610f1 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m @@ -144,7 +144,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - -@interface ConversationViewModel () +@interface ConversationViewModel () @property (nonatomic, weak) id delegate; @@ -241,6 +241,11 @@ NS_ASSUME_NONNULL_BEGIN return [OWSProfileManager shared]; } +- (CallService *)callService +{ + return AppEnvironment.shared.callService; +} + #pragma mark - - (void)addNotificationListeners @@ -265,6 +270,7 @@ NS_ASSUME_NONNULL_BEGIN selector:@selector(localProfileDidChange:) name:kNSNotificationNameLocalProfileDidChange object:nil]; + [self.callService addObserverAndSyncStateWithObserver:self]; } - (void)profileWhitelistDidChange:(NSNotification *)notification @@ -326,6 +332,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)dealloc { + [self.callService removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -1279,6 +1286,12 @@ NS_ASSUME_NONNULL_BEGIN viewItem.senderProfileName = [self.profileManager fullNameForAddress:infoMessage.profileChangeAddress transaction:transaction]; } + } else if (interactionType == OWSInteractionType_Call) { + NSString *activeCallThreadId = self.callService.currentCall.thread.uniqueId; + BOOL isAnotherThreadInCall = activeCallThreadId && ![self.thread.uniqueId isEqualToString:activeCallThreadId]; + + BOOL wasCollapsed = viewItem.shouldCollapseSystemMessageAction; + viewItem.shouldCollapseSystemMessageAction = wasCollapsed || isAnotherThreadInCall; } uint64_t collapseCutoffTimestamp = [NSDate ows_millisecondsSince1970ForDate:self.collapseCutoffDate]; @@ -1494,6 +1507,18 @@ NS_ASSUME_NONNULL_BEGIN return !self.canLoadOlderItems; } +#pragma mark - + +- (void)didUpdateCallFrom:(nullable SignalCall *)oldValue to:(nullable SignalCall *)newValue +{ + // If the call state changed for the current thread, we may want to hide the Call action. + // When registering, CallObserver sends the currentCall synchronously. Since we register + // during -viewDidLoad, ordering gets a bit messed up. It's okay if we async for a bit later. + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateForTransientItems]; + }); +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index d40fe452ec..218414c05f 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -481,6 +481,9 @@ /* notification body */ "CALL_MISSED_BECAUSE_OF_IDENTITY_CHANGE_NOTIFICATION_BODY" = "☎️ Missed call because the caller's safety number changed."; +/* Button to return to the current call */ +"CALL_RETURN_BUTTON" = "Return To Call"; + /* Call setup status label after outgoing call times out */ "CALL_SCREEN_STATUS_NO_ANSWER" = "No Answer";