mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 12:44:38 +00:00
Send reactions.
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<EmojiImageView>
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<GroupCallReactionEvent, WebRt
|
||||
private val textView: EmojiTextView = itemView.findViewById(R.id.webrtc_call_reaction_name_textview)
|
||||
fun bind(item: GroupCallReactionEvent) {
|
||||
emojiView.setImageEmoji(item.reaction)
|
||||
textView.text = item.sender.getRecipientDisplayNameDeviceAgnostic(itemView.context)
|
||||
textView.text = getName(item.sender)
|
||||
itemView.isClickable = false
|
||||
textView.isClickable = false
|
||||
emojiView.isClickable = false
|
||||
}
|
||||
|
||||
private fun getName(recipient: Recipient): String {
|
||||
return if (recipient.isSelf) {
|
||||
itemView.context.getString(R.string.CallParticipant__you)
|
||||
} else {
|
||||
recipient.getDisplayName(itemView.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<GroupCallReactionEvent>() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GroupCallReactionEvent> 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<GroupCall.Reaction> reactions) {
|
||||
List<GroupCallReactionEvent> reactionList = ephemeralState.getUnexpiredReactions();
|
||||
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
|
||||
List<GroupCallReactionEvent> reactionList = ephemeralState.getUnexpiredReactions();
|
||||
List<CallParticipant> 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<CallParticipantId, CallParticipant> 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<CallParticipant> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
15
app/src/main/res/drawable/symbol_more_24.xml
Normal file
15
app/src/main/res/drawable/symbol_more_24.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5.13 13.63c0.9 0 1.62-0.73 1.62-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.62 0.73-1.62 1.63 0 0.9 0.73 1.63 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.63 12c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.63-0.73-1.63-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.63 0.73 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20.5 12c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.62-0.73-1.62-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.62 0.73 1.62 1.63Z"/>
|
||||
</vector>
|
||||
15
app/src/main/res/drawable/symbol_more_white_24.xml
Normal file
15
app/src/main/res/drawable/symbol_more_white_24.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_light_colorOnPrimary"
|
||||
android:pathData="M5.13 13.63c0.9 0 1.62-0.73 1.62-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.62 0.73-1.62 1.63 0 0.9 0.73 1.63 1.63 1.63Z" />
|
||||
<path
|
||||
android:fillColor="@color/signal_light_colorOnPrimary"
|
||||
android:pathData="M13.63 12c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.63-0.73-1.63-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.63 0.73 1.63 1.63Z" />
|
||||
<path
|
||||
android:fillColor="@color/signal_light_colorOnPrimary"
|
||||
android:pathData="M20.5 12c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.62-0.73-1.62-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.62 0.73 1.62 1.63Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/webrtc_call_screen_circle_checked" />
|
||||
<item
|
||||
android:bottom="14dp"
|
||||
android:drawable="@drawable/symbol_more_white_24"
|
||||
android:left="14dp"
|
||||
android:right="14dp"
|
||||
android:top="14dp" />
|
||||
</layer-list>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:bottom="4dp"
|
||||
android:top="4dp"
|
||||
android:left="4dp"
|
||||
android:right="4dp"
|
||||
android:drawable="@drawable/webrtc_call_screen_circle_checked" />
|
||||
<item
|
||||
android:bottom="14dp"
|
||||
android:drawable="@drawable/symbol_more_white_24"
|
||||
android:left="14dp"
|
||||
android:right="14dp"
|
||||
android:top="14dp" />
|
||||
</layer-list>
|
||||
12
app/src/main/res/layout/call_overflow_holder.xml
Normal file
12
app/src/main/res/layout/call_overflow_holder.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2023 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/conversation_reaction_overlay_background"
|
||||
android:elevation="4dp"
|
||||
/>
|
||||
89
app/src/main/res/layout/call_overflow_popup.xml
Normal file
89
app/src/main/res/layout/call_overflow_popup.xml
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2023 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="@dimen/calling_reaction_emoji_height"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1"
|
||||
tools:translationY="0dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="@dimen/calling_reaction_emoji_height"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_3"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="@dimen/calling_reaction_emoji_height"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_4"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_2"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_4"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="@dimen/calling_reaction_emoji_height"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_5"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_5"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="@dimen/calling_reaction_emoji_height"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_6"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_4"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_6"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="@dimen/calling_reaction_emoji_height"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_7"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reaction_7"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="@dimen/calling_reaction_emoji_height"
|
||||
app:forceJumbo="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_6"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
</merge>
|
||||
@@ -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" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_screen_overflow_button"
|
||||
android:layout_width="@dimen/webrtc_button_size"
|
||||
android:layout_height="@dimen/webrtc_button_size"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="30dp"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/WebRtcCallView__additional_actions"
|
||||
android:scaleType="fitXY"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/webrtc_call_screen_circle_checked"
|
||||
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
|
||||
app:layout_constraintEnd_toStartOf="@id/call_screen_end_call"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/call_screen_audio_ring_toggle"
|
||||
app:srcCompat="@drawable/symbol_more_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_screen_end_call"
|
||||
android:layout_width="@dimen/webrtc_button_size"
|
||||
@@ -142,7 +162,7 @@
|
||||
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/call_screen_audio_ring_toggle"
|
||||
app:layout_constraintStart_toEndOf="@id/call_screen_overflow_button"
|
||||
app:srcCompat="@drawable/webrtc_call_screen_hangup"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
<dimen name="reaction_scrubber_anim_start_translation_y">25dp</dimen>
|
||||
<dimen name="reaction_scrubber_anim_end_translation_y">0dp</dimen>
|
||||
<dimen name="reaction_scrubber_width">320dp</dimen>
|
||||
<dimen name="calling_reaction_scrubber_margin">4dp</dimen>
|
||||
<dimen name="calling_reaction_emoji_height">48dp</dimen>
|
||||
|
||||
<dimen name="conversation_item_reply_size">38dp</dimen>
|
||||
<dimen name="conversation_item_avatar_size">28dp</dimen>
|
||||
|
||||
@@ -1831,6 +1831,8 @@
|
||||
<string name="WebRtcCallView__toggle_camera">Toggle camera</string>
|
||||
<!-- Toggle content description for toggling mute state -->
|
||||
<string name="WebRtcCallView__toggle_mute">Toggle mute</string>
|
||||
<!-- Content description for additional actions menu button -->
|
||||
<string name="WebRtcCallView__additional_actions">Additional actions</string>
|
||||
<!-- Content description for end-call button -->
|
||||
<string name="WebRtcCallView__end_call">End call</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user