diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 24be2855b8..8986d874af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -29,7 +29,6 @@ import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.util.Rational; -import android.view.View; import android.view.Window; import android.view.WindowManager; @@ -60,6 +59,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor; import org.thoughtcrime.securesms.components.webrtc.CallLinkInfoSheet; import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender; +import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow; import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow; import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState; import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow; @@ -82,6 +82,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity; import org.thoughtcrime.securesms.permissions.Permissions; +import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -115,7 +116,7 @@ import io.reactivex.rxjava3.disposables.Disposable; import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; -public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback { +public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback { private static final String TAG = Log.tag(WebRtcCallActivity.class); @@ -140,6 +141,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan private CallParticipantsListUpdatePopupWindow participantUpdateWindow; private CallStateUpdatePopupWindow callStateUpdatePopupWindow; + private CallOverflowPopupWindow callOverflowPopupWindow; private WifiToCellularPopupWindow wifiToCellularPopupWindow; private DeviceOrientationMonitor deviceOrientationMonitor; @@ -193,7 +195,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan initializeViewModel(isLandscapeEnabled); initializePictureInPictureParams(); - controlsAndInfo = new ControlsAndInfoController(callScreen, viewModel); + controlsAndInfo = new ControlsAndInfoController(callScreen, callOverflowPopupWindow, viewModel); controlsAndInfo.addVisibilityListener(new FadeCallback()); fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.webrtc_call_view_toolbar_text), findViewById(R.id.webrtc_call_view_toolbar_no_text)); @@ -432,6 +434,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen); callStateUpdatePopupWindow = new CallStateUpdatePopupWindow(callScreen); wifiToCellularPopupWindow = new WifiToCellularPopupWindow(callScreen); + callOverflowPopupWindow = new CallOverflowPopupWindow(this, callScreen); } private void initializeViewModel(boolean isLandscapeEnabled) { @@ -947,6 +950,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan MessageSender.onMessageSent(); } + @Override + public void onReactWithAnyEmojiDialogDismissed() { /* no-op */ } + + @Override + public void onReactWithAnyEmojiSelected(@NonNull String emoji) { + ApplicationDependencies.getSignalCallManager().react(emoji); + callOverflowPopupWindow.dismiss(); + } + private final class ControlsListener implements WebRtcCallView.ControlsListener { @Override @@ -1034,6 +1046,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan handleAnswerWithAudio(); } + @Override + public void onOverflowClicked() { + controlsAndInfo.toggleOverflowPopup(); + } + @Override public void onAcceptCallPressed() { if (viewModel.isAnswerWithVideoAvailable()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt new file mode 100644 index 0000000000..6c72155f2b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc + +import android.graphics.Rect +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import androidx.core.widget.PopupWindowCompat +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.WebRtcCallActivity +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies + +/** + * A popup window for calls that holds extra actions, such as reactions, raise hand, and screen sharing. + * + */ +class CallOverflowPopupWindow(private val activity: WebRtcCallActivity, parentViewGroup: ViewGroup) : PopupWindow( + LayoutInflater.from(activity).inflate(R.layout.call_overflow_holder, parentViewGroup, false), + activity.resources.getDimension(R.dimen.reaction_scrubber_width).toInt(), + ViewGroup.LayoutParams.WRAP_CONTENT +) { + + init { + (contentView as CallReactionScrubber).initialize(activity.supportFragmentManager, activity) { + ApplicationDependencies.getSignalCallManager().react(it) + dismiss() + } + } + + fun show(anchor: View) { + isFocusable = true + + val resources = activity.resources + + val margin = resources.getDimension(R.dimen.calling_reaction_scrubber_margin).toInt() + + val windowRect = Rect() + contentView.getWindowVisibleDisplayFrame(windowRect) + val windowWidth = windowRect.width() + val popupWidth = resources.getDimension(R.dimen.reaction_scrubber_width).toInt() + + val popupHeight = resources.getDimension(R.dimen.calling_reaction_emoji_height).toInt() + + val xOffset = windowWidth - popupWidth - margin + val yOffset = -popupHeight - margin + + PopupWindowCompat.showAsDropDown(this, anchor, xOffset, yOffset, Gravity.NO_GRAVITY) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt new file mode 100644 index 0000000000..d0d26115f0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentManager +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.emoji.EmojiImageView +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment + +class CallReactionScrubber @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val emojiViews: Array + private var customEmojiIndex = 0 + + init { + inflate(context, R.layout.call_overflow_popup, this) + + emojiViews = arrayOf( + findViewById(R.id.reaction_1), + findViewById(R.id.reaction_2), + findViewById(R.id.reaction_3), + findViewById(R.id.reaction_4), + findViewById(R.id.reaction_5), + findViewById(R.id.reaction_6), + findViewById(R.id.reaction_7) + ) + customEmojiIndex = emojiViews.size - 1 + } + + fun initialize(fragmentManager: FragmentManager, callback: ReactWithAnyEmojiBottomSheetDialogFragment.Callback, listener: (String) -> Unit) { + val emojis = SignalStore.emojiValues().reactions + for (i in emojiViews.indices) { + val view = emojiViews[i] + val isAtCustomIndex = i == customEmojiIndex + if (isAtCustomIndex) { + view.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_any_emoji_32)) + view.setOnClickListener { + val bottomSheet = ReactWithAnyEmojiBottomSheetDialogFragment.createForCallingReactions() + bottomSheet.show(fragmentManager, "CallReaction") + } + } else { + val preferredVariation = SignalStore.emojiValues().getPreferredVariation(emojis[i]) + view.setImageEmoji(preferredVariation) + view.setOnClickListener { listener(preferredVariation) } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index 765ab0a43f..51c230bf0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -37,7 +37,6 @@ import androidx.viewpager2.widget.ViewPager2; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.google.android.material.button.MaterialButton; -import com.google.common.collect.Sets; import org.signal.core.util.DimensionUnit; import org.signal.core.util.SetUtil; @@ -98,6 +97,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private ImageView cameraDirectionToggle; private AccessibleToggleButton ringToggle; private PictureInPictureGestureHelper pictureInPictureGestureHelper; + private ImageView overflow; private ImageView hangup; private View answerWithoutVideo; private View topGradient; @@ -179,6 +179,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { answerWithoutVideoLabel = findViewById(R.id.call_screen_answer_without_video_label); cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle); ringToggle = findViewById(R.id.call_screen_audio_ring_toggle); + overflow = findViewById(R.id.call_screen_overflow_button); hangup = findViewById(R.id.call_screen_end_call); answerWithoutVideo = findViewById(R.id.call_screen_answer_without_video); topGradient = findViewById(R.id.call_screen_header_gradient); @@ -278,6 +279,10 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged)); + overflow.setOnClickListener(v -> { + runIfNonNull(controlsListener, ControlsListener::onOverflowClicked); + }); + hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed)); decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed)); @@ -346,6 +351,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { return false; }); + rotatableControls.add(overflow); rotatableControls.add(hangup); rotatableControls.add(answer); rotatableControls.add(answerWithoutVideo); @@ -596,6 +602,10 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { } } + public @NonNull View getPopupAnchor() { + return aboveControlsGuideline; + } + public void setStatusFromHangupType(@NonNull HangupMessage.Type hangupType) { switch (hangupType) { case NORMAL: @@ -717,6 +727,10 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { visibleViewSet.add(footerGradient); } + if (webRtcControls.displayOverflow()) { + visibleViewSet.add(overflow); + } + if (webRtcControls.displayMuteAudio()) { visibleViewSet.add(micToggle); } @@ -893,6 +907,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private void updateButtonStateForLargeButtons() { cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle); hangup.setImageResource(R.drawable.webrtc_call_screen_hangup); + overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu); micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle); videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle); audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle); @@ -902,6 +917,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private void updateButtonStateForSmallButtons() { cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle_small); hangup.setImageResource(R.drawable.webrtc_call_screen_hangup_small); + overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu_small); micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small); videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle_small); audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small); @@ -938,6 +954,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput); void onVideoChanged(boolean isVideoEnabled); void onMicChanged(boolean isMicEnabled); + void onOverflowClicked(); void onCameraDirectionChanged(); void onEndCallPressed(); void onDenyCallPressed(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index 4f8d7eb674..497f3ff70e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -8,6 +8,7 @@ import androidx.annotation.Px; import androidx.annotation.StringRes; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import java.util.Set; @@ -155,6 +156,10 @@ public final class WebRtcControls { return isAtLeastOutgoing() || callState == CallState.RECONNECTING; } + public boolean displayOverflow() { + return FeatureFlags.groupCallReactions() && isAtLeastOutgoing(); + } + public boolean displayMuteAudio() { return isPreJoin() || isAtLeastOutgoing(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcReactionsRecyclerAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcReactionsRecyclerAdapter.kt index 9854d8f8d7..d82413e045 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcReactionsRecyclerAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcReactionsRecyclerAdapter.kt @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.emoji.EmojiImageView import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.events.GroupCallReactionEvent +import org.thoughtcrime.securesms.recipients.Recipient /** * RecyclerView adapter for the reactions feed. This takes in a list of [GroupCallReactionEvent] and renders them onto the screen. @@ -48,11 +49,19 @@ class WebRtcReactionsRecyclerAdapter : ListAdapter() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt index 8b29254137..fb2f543e8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt @@ -28,6 +28,7 @@ import io.reactivex.rxjava3.disposables.Disposable import org.signal.core.util.dp import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout +import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel import org.thoughtcrime.securesms.components.webrtc.WebRtcControls @@ -41,6 +42,7 @@ import kotlin.time.Duration.Companion.seconds */ class ControlsAndInfoController( private val webRtcCallView: WebRtcCallView, + private val overflowPopupWindow: CallOverflowPopupWindow, private val viewModel: WebRtcCallViewModel ) : Disposable { @@ -122,6 +124,7 @@ class ControlsAndInfoController( behavior.addBottomSheetCallback(object : BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { + overflowPopupWindow.dismiss() if (newState == BottomSheetBehavior.STATE_COLLAPSED) { if (controlState.isFadeOutEnabled) { hide(delay = HIDE_CONTROL_DELAY) @@ -149,6 +152,10 @@ class ControlsAndInfoController( } } }) + + overflowPopupWindow.setOnDismissListener { + hide(delay = HIDE_CONTROL_DELAY) + } } fun addVisibilityListener(listener: BottomSheetVisibilityListener): Boolean { @@ -191,6 +198,15 @@ class ControlsAndInfoController( } } + fun toggleOverflowPopup() { + if (overflowPopupWindow.isShowing) { + overflowPopupWindow.dismiss() + } else { + cancelScheduledHide() + overflowPopupWindow.show(webRtcCallView.popupAnchor) + } + } + fun updateControls(newControlState: WebRtcControls) { val previousState = controlState controlState = newControlState diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt index 5e89f6813d..5583b3c978 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt @@ -47,14 +47,6 @@ data class CallParticipant constructor( } } - fun getRecipientDisplayNameDeviceAgnostic(context: Context): String { - return if (recipient.isSelf) { - context.getString(R.string.CallParticipant__you) - } else { - recipient.getDisplayName(context) - } - } - fun getShortRecipientDisplayName(context: Context): String { return if (recipient.isSelf && isPrimary) { context.getString(R.string.CallParticipant__you) diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallReactionEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallReactionEvent.kt index 7b1766e758..6de2582a4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallReactionEvent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallReactionEvent.kt @@ -5,13 +5,14 @@ package org.thoughtcrime.securesms.events +import org.thoughtcrime.securesms.recipients.Recipient import java.util.concurrent.TimeUnit /** - * This is a data class to represent a reaction coming in over the wire in the format we need (mapped to a [CallParticipant]) in a way that can be easily + * This is a data class to represent a reaction coming in over the wire in the format we need (mapped to a [Recipient]) in a way that can be easily * compared across Rx streams. */ -data class GroupCallReactionEvent(val sender: CallParticipant, val reaction: String, val timestamp: Long) { +data class GroupCallReactionEvent(val sender: Recipient, val reaction: String, val timestamp: Long) { fun getExpirationTimestamp(): Long { return timestamp + TimeUnit.SECONDS.toMillis(LIFESPAN_SECONDS) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java index 71b76d0fc1..16ae57fcc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java @@ -59,7 +59,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound private static final String ARG_EDIT = "arg_edit"; private ReactWithAnyEmojiViewModel viewModel; - private Callback callback; + private Callback callback = null; private EmojiPageView emojiPageView; private KeyboardPageSearchView search; private View tabBar; @@ -123,6 +123,20 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound return fragment; } + public static ReactWithAnyEmojiBottomSheetDialogFragment createForCallingReactions() { + ReactWithAnyEmojiBottomSheetDialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment(); + Bundle args = new Bundle(); + + args.putLong(ARG_MESSAGE_ID, -1); + args.putBoolean(ARG_IS_MMS, false); + args.putInt(ARG_START_PAGE, -1); + args.putBoolean(ARG_SHADOWS, false); + args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY); + fragment.setArguments(args); + + return fragment; + } + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -229,8 +243,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); - - callback.onReactWithAnyEmojiDialogDismissed(); + if (callback != null) callback.onReactWithAnyEmojiDialogDismissed(); } private void initializeViewModel() { @@ -244,7 +257,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound @Override public void onEmojiSelected(String emoji) { viewModel.onEmojiSelected(emoji); - callback.onReactWithAnyEmojiSelected(emoji); + if (callback != null) callback.onReactWithAnyEmojiSelected(emoji); dismiss(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java index 5a7e5e74b4..579e8e3cb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java @@ -20,16 +20,16 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.RemotePeer; +import org.thoughtcrime.securesms.service.webrtc.state.CallInfoState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.UUID; -import java.util.stream.Collectors; /** * Process actions for when the call has at least once been connected and joined. @@ -202,10 +202,25 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { return terminateGroupCall(currentState); } + @Override + protected @NonNull WebRtcEphemeralState handleSendGroupReact(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState, @NonNull String reaction) { + try { + currentState.getCallInfoState().requireGroupCall().react(reaction); + + List reactionList = ephemeralState.getUnexpiredReactions(); + reactionList.add(new GroupCallReactionEvent(Recipient.self(), reaction, System.currentTimeMillis())); + + return ephemeralState.copy(ephemeralState.getLocalAudioLevel(), ephemeralState.getRemoteAudioLevels(), reactionList); + } catch (CallException e) { + Log.w(TAG,"Unable to send reaction in group call", e); + } + return ephemeralState; + } + @Override protected @NonNull WebRtcEphemeralState handleGroupCallReaction(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState, List reactions) { - List reactionList = ephemeralState.getUnexpiredReactions(); - Map participants = currentState.getCallInfoState().getRemoteCallParticipantsMap(); + List reactionList = ephemeralState.getUnexpiredReactions(); + List participants = currentState.getCallInfoState().getRemoteCallParticipants(); for (GroupCall.Reaction reaction : reactions) { final GroupCallReactionEvent event = createGroupCallReaction(participants, reaction); @@ -218,13 +233,13 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { } @Nullable - private GroupCallReactionEvent createGroupCallReaction(Map participants, final GroupCall.Reaction reaction) { - CallParticipantId participantId = participants.keySet().stream().filter(participant -> participant.getDemuxId() == reaction.demuxId).findFirst().orElse(null); - if (participantId == null) { + private GroupCallReactionEvent createGroupCallReaction(Collection participants, final GroupCall.Reaction reaction) { + CallParticipant participant = participants.stream().filter(it -> it.getCallParticipantId().getDemuxId() == reaction.demuxId).findFirst().orElse(null); + if (participant == null) { Log.v(TAG, "Could not find CallParticipantId in list of call participants based on demuxId for reaction."); return null; } - return new GroupCallReactionEvent(participants.get(participantId), reaction.value, System.currentTimeMillis()); + return new GroupCallReactionEvent(participant.getRecipient(), reaction.value, System.currentTimeMillis()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index b994a1e24f..f339bcdc21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -295,8 +295,8 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. process((s, p) -> p.handleScreenOffChange(s)); } - public void react() { - process((s, p) -> p.handleSendGroupReact(s)); + public void react(@NonNull String reaction) { + processStateless(s -> serviceState.getActionProcessor().handleSendGroupReact(serviceState, s, reaction)); } public void postStateUpdate(@NonNull WebRtcServiceState state) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 381d52c805..9d30723d53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -546,9 +546,9 @@ public abstract class WebRtcActionProcessor { return currentState; } - protected @NonNull WebRtcServiceState handleSendGroupReact(@NonNull WebRtcServiceState currentState) { + protected @NonNull WebRtcEphemeralState handleSendGroupReact(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState, @NonNull String reaction) { Log.i(tag, "react not processed"); - return currentState; + return ephemeralState; } public @NonNull WebRtcServiceState handleCameraSwitchCompleted(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) { diff --git a/app/src/main/res/drawable/symbol_more_24.xml b/app/src/main/res/drawable/symbol_more_24.xml new file mode 100644 index 0000000000..b496d9c464 --- /dev/null +++ b/app/src/main/res/drawable/symbol_more_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/symbol_more_white_24.xml b/app/src/main/res/drawable/symbol_more_white_24.xml new file mode 100644 index 0000000000..a254159c47 --- /dev/null +++ b/app/src/main/res/drawable/symbol_more_white_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/webrtc_call_screen_overflow_menu.xml b/app/src/main/res/drawable/webrtc_call_screen_overflow_menu.xml new file mode 100644 index 0000000000..e529dc6d0b --- /dev/null +++ b/app/src/main/res/drawable/webrtc_call_screen_overflow_menu.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/webrtc_call_screen_overflow_menu_small.xml b/app/src/main/res/drawable/webrtc_call_screen_overflow_menu_small.xml new file mode 100644 index 0000000000..f8177e2754 --- /dev/null +++ b/app/src/main/res/drawable/webrtc_call_screen_overflow_menu_small.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_overflow_holder.xml b/app/src/main/res/layout/call_overflow_holder.xml new file mode 100644 index 0000000000..90d618dc57 --- /dev/null +++ b/app/src/main/res/layout/call_overflow_holder.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_overflow_popup.xml b/app/src/main/res/layout/call_overflow_popup.xml new file mode 100644 index 0000000000..8fc37bcd3d --- /dev/null +++ b/app/src/main/res/layout/call_overflow_popup.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_controls.xml b/app/src/main/res/layout/webrtc_call_controls.xml index 6c1a185c48..ac3ce2c683 100644 --- a/app/src/main/res/layout/webrtc_call_controls.xml +++ b/app/src/main/res/layout/webrtc_call_controls.xml @@ -124,10 +124,30 @@ android:stateListAnimator="@null" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls" - app:layout_constraintEnd_toStartOf="@id/call_screen_end_call" + app:layout_constraintEnd_toStartOf="@id/call_screen_overflow_button" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toEndOf="@id/call_screen_audio_mic_toggle" /> + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f33bd73ed6..09b2d859d7 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -79,6 +79,8 @@ 25dp 0dp 320dp + 4dp + 48dp 38dp 28dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8a72831bf..965f6077ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1831,6 +1831,8 @@ Toggle camera Toggle mute + + Additional actions End call