mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Refactor how message send types are selected.
This commit is contained in:
@@ -28,12 +28,12 @@ import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionDeleter;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -201,13 +201,13 @@ public class ComposeText extends EmojiEditText {
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
public void setTransport(TransportOption transport) {
|
||||
public void setMessageSendType(MessageSendType messageSendType) {
|
||||
final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
|
||||
|
||||
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
|
||||
int inputType = getInputType();
|
||||
|
||||
if (isLandscape()) setImeActionLabel(transport.getComposeHint(), EditorInfo.IME_ACTION_SEND);
|
||||
if (isLandscape()) setImeActionLabel(getContext().getString(messageSendType.getComposeHintRes()), EditorInfo.IME_ACTION_SEND);
|
||||
else setImeActionLabel(null, 0);
|
||||
|
||||
if (useSystemEmoji) {
|
||||
@@ -215,9 +215,9 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
setImeOptions(imeOptions);
|
||||
setHint(transport.getComposeHint(),
|
||||
transport.getSimName().isPresent()
|
||||
? getContext().getString(R.string.conversation_activity__from_sim_name, transport.getSimName().get())
|
||||
setHint(getContext().getString(messageSendType.getComposeHintRes()),
|
||||
messageSendType.getSimName() != null
|
||||
? getContext().getString(R.string.conversation_activity__from_sim_name, messageSendType.getSimName())
|
||||
: null);
|
||||
setInputType(inputType);
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
|
||||
import org.thoughtcrime.securesms.TransportOptionsPopup;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class SendButton extends AppCompatImageButton
|
||||
implements TransportOptions.OnTransportChangedListener,
|
||||
TransportOptionsPopup.SelectedListener,
|
||||
View.OnLongClickListener
|
||||
{
|
||||
|
||||
private final TransportOptions transportOptions;
|
||||
|
||||
private Optional<TransportOptionsPopup> transportOptionsPopup = Optional.empty();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SendButton(Context context) {
|
||||
super(context);
|
||||
this.transportOptions = initializeTransportOptions(false);
|
||||
ViewUtil.mirrorIfRtl(this, getContext());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SendButton(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.transportOptions = initializeTransportOptions(false);
|
||||
ViewUtil.mirrorIfRtl(this, getContext());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SendButton(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
this.transportOptions = initializeTransportOptions(false);
|
||||
ViewUtil.mirrorIfRtl(this, getContext());
|
||||
}
|
||||
|
||||
private TransportOptions initializeTransportOptions(boolean media) {
|
||||
if (isInEditMode()) return null;
|
||||
|
||||
TransportOptions transportOptions = new TransportOptions(getContext(), media);
|
||||
transportOptions.addOnTransportChangedListener(this);
|
||||
|
||||
setOnLongClickListener(this);
|
||||
|
||||
return transportOptions;
|
||||
}
|
||||
|
||||
private TransportOptionsPopup getTransportOptionsPopup() {
|
||||
if (!transportOptionsPopup.isPresent()) {
|
||||
transportOptionsPopup = Optional.of(new TransportOptionsPopup(getContext(), this, this));
|
||||
}
|
||||
return transportOptionsPopup.get();
|
||||
}
|
||||
|
||||
public boolean isManualSelection() {
|
||||
return transportOptions.isManualSelection();
|
||||
}
|
||||
|
||||
public void addOnTransportChangedListener(OnTransportChangedListener listener) {
|
||||
transportOptions.addOnTransportChangedListener(listener);
|
||||
}
|
||||
|
||||
public TransportOption getSelectedTransport() {
|
||||
return transportOptions.getSelectedTransport();
|
||||
}
|
||||
|
||||
public void resetAvailableTransports(boolean isMediaMessage) {
|
||||
transportOptions.reset(isMediaMessage);
|
||||
}
|
||||
|
||||
public void disableTransport(TransportOption.Type type) {
|
||||
transportOptions.disableTransport(type);
|
||||
}
|
||||
|
||||
public void setDefaultTransport(TransportOption.Type type) {
|
||||
transportOptions.setDefaultTransport(type);
|
||||
}
|
||||
|
||||
public void setTransport(@NonNull TransportOption option) {
|
||||
transportOptions.setSelectedTransport(option);
|
||||
}
|
||||
|
||||
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
|
||||
transportOptions.setDefaultSubscriptionId(subscriptionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelected(TransportOption option) {
|
||||
transportOptions.setSelectedTransport(option);
|
||||
getTransportOptionsPopup().dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(TransportOption newTransport, boolean isManualSelection) {
|
||||
setImageResource(newTransport.getDrawable());
|
||||
setContentDescription(newTransport.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (isEnabled() && transportOptions.getEnabledTransports().size() > 1) {
|
||||
getTransportOptionsPopup().display(transportOptions.getEnabledTransports());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import java.lang.AssertionError
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* The send button you see in a conversation.
|
||||
* Also encapsulates the long-press menu that allows users to switch [MessageSendType]s.
|
||||
*/
|
||||
class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImageButton(context, attributeSet), OnLongClickListener {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SendButton::class.java)
|
||||
}
|
||||
|
||||
private val listeners: MutableList<SendTypeChangedListener> = CopyOnWriteArrayList()
|
||||
|
||||
private var availableSendTypes: List<MessageSendType> = MessageSendType.getAllAvailable(context, false)
|
||||
private var activeMessageSendType: MessageSendType? = null
|
||||
private var defaultTransportType: MessageSendType.TransportType = MessageSendType.TransportType.SMS
|
||||
private var defaultSubscriptionId: Int? = null
|
||||
private var popupContainer: ViewGroup? = null
|
||||
|
||||
init {
|
||||
setOnLongClickListener(this)
|
||||
ViewUtil.mirrorIfRtl(this, getContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the [selectedSendType] was chosen manually by the user, otherwise false.
|
||||
*/
|
||||
val isManualSelection: Boolean
|
||||
get() = activeMessageSendType != null
|
||||
|
||||
/**
|
||||
* The actively-selected send type.
|
||||
*/
|
||||
val selectedSendType: MessageSendType
|
||||
get() {
|
||||
activeMessageSendType?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
if (defaultTransportType === MessageSendType.TransportType.SMS) {
|
||||
for (type in availableSendTypes) {
|
||||
if (type.usesSmsTransport && (defaultSubscriptionId == null || type.simSubscriptionId == defaultSubscriptionId)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (type in availableSendTypes) {
|
||||
if (type.transportType === defaultTransportType) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
throw AssertionError("No options of default type!")
|
||||
}
|
||||
|
||||
fun addOnSelectionChangedListener(listener: SendTypeChangedListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun triggerSelectedChangedEvent() {
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun resetAvailableTransports(isMediaMessage: Boolean) {
|
||||
availableSendTypes = MessageSendType.getAllAvailable(context, isMediaMessage)
|
||||
|
||||
if (!availableSendTypes.contains(activeMessageSendType)) {
|
||||
Log.w(TAG, "[resetAvailableTransports] The active send type is no longer available. Unsetting.")
|
||||
setSendType(null)
|
||||
} else {
|
||||
defaultTransportType = MessageSendType.TransportType.SMS
|
||||
defaultSubscriptionId = null
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableTransportType(type: MessageSendType.TransportType) {
|
||||
availableSendTypes = availableSendTypes.filterNot { it.transportType == type }
|
||||
}
|
||||
|
||||
fun setDefaultTransport(type: MessageSendType.TransportType) {
|
||||
if (defaultTransportType == type) {
|
||||
return
|
||||
}
|
||||
defaultTransportType = type
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun setSendType(sendType: MessageSendType?) {
|
||||
if (activeMessageSendType == sendType) {
|
||||
return
|
||||
}
|
||||
activeMessageSendType = sendType
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = true)
|
||||
}
|
||||
|
||||
fun setDefaultSubscriptionId(subscriptionId: Int?) {
|
||||
if (defaultSubscriptionId == subscriptionId) {
|
||||
return
|
||||
}
|
||||
defaultSubscriptionId = subscriptionId
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called with a view that is acceptable for determining the bounds of the popup selector.
|
||||
*/
|
||||
fun setPopupContainer(container: ViewGroup) {
|
||||
popupContainer = container
|
||||
}
|
||||
|
||||
private fun onSelectionChanged(newType: MessageSendType, isManualSelection: Boolean) {
|
||||
setImageResource(newType.buttonDrawableRes)
|
||||
contentDescription = context.getString(newType.titleRes)
|
||||
|
||||
for (listener in listeners) {
|
||||
listener.onSendTypeChanged(newType, isManualSelection)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
if (!isEnabled || availableSendTypes.size == 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
val currentlySelected: MessageSendType = selectedSendType
|
||||
|
||||
val items = availableSendTypes
|
||||
.filterNot { it == currentlySelected }
|
||||
.map { option ->
|
||||
ActionItem(
|
||||
iconRes = option.menuDrawableRes,
|
||||
title = option.getTitle(context),
|
||||
action = { setSendType(option) }
|
||||
)
|
||||
}
|
||||
|
||||
SignalContextMenu.Builder((parent as View), popupContainer!!)
|
||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.ABOVE)
|
||||
.offsetY(ViewUtil.dpToPx(8))
|
||||
.show(items)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
interface SendTypeChangedListener {
|
||||
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ class SignalContextMenu private constructor(
|
||||
val baseOffsetX: Int = 0,
|
||||
val baseOffsetY: Int = 0,
|
||||
val horizontalPosition: HorizontalPosition = HorizontalPosition.START,
|
||||
val verticalPosition: VerticalPosition = VerticalPosition.BELOW,
|
||||
val onDismiss: Runnable? = null
|
||||
) : PopupWindow(
|
||||
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
|
||||
@@ -41,6 +42,7 @@ class SignalContextMenu private constructor(
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
||||
|
||||
isFocusable = true
|
||||
|
||||
@@ -80,7 +82,10 @@ class SignalContextMenu private constructor(
|
||||
|
||||
val offsetY: Int
|
||||
|
||||
if (menuBottomBound < screenBottomBound) {
|
||||
if (verticalPosition == VerticalPosition.ABOVE && menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||
contextMenuList.setItems(items.reversed())
|
||||
} else if (menuBottomBound < screenBottomBound) {
|
||||
offsetY = baseOffsetY
|
||||
} else if (menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||
@@ -115,6 +120,10 @@ class SignalContextMenu private constructor(
|
||||
START, END
|
||||
}
|
||||
|
||||
enum class VerticalPosition {
|
||||
ABOVE, BELOW
|
||||
}
|
||||
|
||||
/**
|
||||
* @param anchor The view to put the pop-up on
|
||||
* @param container A parent of [anchor] that represents the acceptable boundaries of the popup
|
||||
@@ -128,6 +137,7 @@ class SignalContextMenu private constructor(
|
||||
var offsetX = 0
|
||||
var offsetY = 0
|
||||
var horizontalPosition = HorizontalPosition.START
|
||||
var verticalPosition = VerticalPosition.BELOW
|
||||
|
||||
fun onDismiss(onDismiss: Runnable): Builder {
|
||||
this.onDismiss = onDismiss
|
||||
@@ -149,6 +159,11 @@ class SignalContextMenu private constructor(
|
||||
return this
|
||||
}
|
||||
|
||||
fun preferredVerticalPosition(verticalPosition: VerticalPosition): Builder {
|
||||
this.verticalPosition = verticalPosition
|
||||
return this
|
||||
}
|
||||
|
||||
fun show(items: List<ActionItem>): SignalContextMenu {
|
||||
return SignalContextMenu(
|
||||
anchor = anchor,
|
||||
@@ -157,6 +172,7 @@ class SignalContextMenu private constructor(
|
||||
baseOffsetX = offsetX,
|
||||
baseOffsetY = offsetY,
|
||||
horizontalPosition = horizontalPosition,
|
||||
verticalPosition = verticalPosition,
|
||||
onDismiss = onDismiss
|
||||
).show()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user