CallLink treatment for ConversationItem.

This commit is contained in:
Alex Hart 2023-06-05 13:55:01 -03:00 committed by Cody Henthorne
parent 8f96abb41e
commit 93df01e266
15 changed files with 180 additions and 15 deletions

View file

@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState; import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationItem; import org.thoughtcrime.securesms.conversation.ConversationItem;
@ -116,5 +117,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args); void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord); void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks); void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
} }
} }

View file

@ -0,0 +1,28 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.calls.links
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.LinearLayoutCompat
import com.google.android.material.button.MaterialButton
import org.thoughtcrime.securesms.R
class CallLinkJoinButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : LinearLayoutCompat(context, attrs) {
init {
orientation = VERTICAL
inflate(context, R.layout.call_link_join_button, this)
}
private val joinButton: MaterialButton = findViewById(R.id.join_button)
fun setJoinClickListener(onClickListener: OnClickListener) {
joinButton.setOnClickListener(onClickListener)
}
}

View file

@ -21,10 +21,11 @@ import java.net.URLDecoder
*/ */
object CallLinks { object CallLinks {
private const val ROOT_KEY = "key" private const val ROOT_KEY = "key"
private const val LINK_PREFIX = "https://signal.link/call/#key="
private val TAG = Log.tag(CallLinks::class.java) private val TAG = Log.tag(CallLinks::class.java)
fun url(linkKeyBytes: ByteArray) = "https://signal.link/call/#key=${Hex.dump(linkKeyBytes)}" fun url(linkKeyBytes: ByteArray) = "$LINK_PREFIX${Hex.dump(linkKeyBytes)}"
fun watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> { fun watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> {
return Observable.create { emitter -> return Observable.create { emitter ->
@ -51,6 +52,10 @@ object CallLinks {
@JvmStatic @JvmStatic
fun parseUrl(url: String): CallLinkRootKey? { fun parseUrl(url: String): CallLinkRootKey? {
if (!url.startsWith(LINK_PREFIX)) {
return null
}
val parts = url.split("#") val parts = url.split("#")
if (parts.size != 2) { if (parts.size != 2) {
Log.w(TAG, "Invalid fragment delimiter count in url.") Log.w(TAG, "Invalid fragment delimiter count in url.")

View file

@ -16,12 +16,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.calls.links.CallLinks;
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
@ -202,11 +206,23 @@ public class LinkPreviewView extends FrameLayout {
site.setVisibility(GONE); site.setVisibility(GONE);
} }
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
if (showThumbnail && linkPreview.getThumbnail().isPresent()) { if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE); thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail); thumbnailState.applyState(thumbnail);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false); thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
thumbnail.get().showDownloadText(false); thumbnail.get().showDownloadText(false);
} else if (callLinkRootKey != null) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageDrawable(
glideRequests,
Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER
.getPhotoForCallLink()
.asDrawable(getContext(),
AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes()))
);
thumbnail.get().showDownloadText(false);
} else { } else {
thumbnail.setVisibility(GONE); thumbnail.setVisibility(GONE);
} }

View file

@ -76,6 +76,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@ -2088,6 +2089,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
GroupDescriptionDialog.show(getChildFragmentManager(), groupName, description, shouldLinkifyWebLinks); GroupDescriptionDialog.show(getChildFragmentManager(), groupName, description, shouldLinkifyWebLinks);
} }
@Override
public void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey) {
CommunicationActions.startVideoCall(ConversationFragment.this, callLinkRootKey);
}
@Override @Override
public void onActivatePaymentsClicked() { public void onActivatePaymentsClicked() {
Intent intent = new Intent(requireContext(), PaymentsActivity.class); Intent intent = new Intent(requireContext(), PaymentsActivity.class);

View file

@ -69,12 +69,15 @@ import com.google.common.collect.Sets;
import org.signal.core.util.DimensionUnit; import org.signal.core.util.DimensionUnit;
import org.signal.core.util.StringUtil; import org.signal.core.util.StringUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.gifts.GiftMessageView; import org.thoughtcrime.securesms.badges.gifts.GiftMessageView;
import org.thoughtcrime.securesms.badges.gifts.OpenableGift; import org.thoughtcrime.securesms.badges.gifts.OpenableGift;
import org.thoughtcrime.securesms.calls.links.CallLinkJoinButton;
import org.thoughtcrime.securesms.calls.links.CallLinks;
import org.thoughtcrime.securesms.components.AlertView; import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
@ -130,6 +133,7 @@ import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageView; import org.thoughtcrime.securesms.revealable.ViewOnceMessageView;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan; import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LinkUtil; import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.LongClickMovementMethod; import org.thoughtcrime.securesms.util.LongClickMovementMethod;
@ -224,6 +228,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private Stub<LinkPreviewView> linkPreviewStub; private Stub<LinkPreviewView> linkPreviewStub;
private Stub<BorderlessImageView> stickerStub; private Stub<BorderlessImageView> stickerStub;
private Stub<ViewOnceMessageView> revealableStub; private Stub<ViewOnceMessageView> revealableStub;
private Stub<CallLinkJoinButton> joinCallLinkStub;
private Stub<Button> callToActionStub; private Stub<Button> callToActionStub;
private Stub<GiftMessageView> giftViewStub; private Stub<GiftMessageView> giftViewStub;
private Stub<PaymentMessageView> paymentViewStub; private Stub<PaymentMessageView> paymentViewStub;
@ -323,6 +328,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub)); this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub));
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub)); this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub)); this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
this.joinCallLinkStub = ViewUtil.findStubById(this, R.id.conversation_item_join_button);
this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub); this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub);
this.groupSenderHolder = findViewById(R.id.group_sender_holder); this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view); this.quoteView = findViewById(R.id.quote_view);
@ -1079,6 +1085,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
if (callToActionStub.resolved()) callToActionStub.get().setVisibility(View.GONE);
paymentViewStub.setVisibility(View.GONE); paymentViewStub.setVisibility(View.GONE);
revealableStub.get().setMessage((MmsMessageRecord) messageRecord, hasWallpaper); revealableStub.get().setMessage((MmsMessageRecord) messageRecord, hasWallpaper);
@ -1123,6 +1130,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
//noinspection ConstantConditions //noinspection ConstantConditions
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
if (FeatureFlags.adHocCalling()) {
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
if (callLinkRootKey != null) {
joinCallLinkStub.setVisibility(View.VISIBLE);
joinCallLinkStub.get().setJoinClickListener(v -> {
if (eventListener != null) {
eventListener.onJoinCallLink(callLinkRootKey);
}
});
}
}
if (hasBigImageLinkPreview(messageRecord)) { if (hasBigImageLinkPreview(messageRecord)) {
mediaThumbnailStub.require().setVisibility(VISIBLE); mediaThumbnailStub.require().setVisibility(VISIBLE);
mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content)); mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content));

View file

@ -37,6 +37,7 @@ object AvatarColorHash {
return forData(seed.toByteArray()) return forData(seed.toByteArray())
} }
@JvmStatic
fun forCallLink(rootKey: ByteArray): AvatarColor { fun forCallLink(rootKey: ByteArray): AvatarColor {
return forIndex(rootKey.first().toInt()) return forIndex(rootKey.first().toInt())
} }

View file

@ -77,6 +77,7 @@ import org.signal.core.util.dp
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.signal.libsignal.protocol.InvalidMessageException import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.BlockUnblockDialog import org.thoughtcrime.securesms.BlockUnblockDialog
import org.thoughtcrime.securesms.GroupMembersDialog import org.thoughtcrime.securesms.GroupMembersDialog
import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.LoggingFragment
@ -1925,6 +1926,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks) GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks)
} }
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey)
}
private fun MessageRecord.getAudioUriForLongClick(): Uri? { private fun MessageRecord.getAudioUriForLongClick(): Uri? {
val playbackState = getVoiceNoteMediaController().voiceNotePlaybackState.value val playbackState = getVoiceNoteMediaController().voiceNotePlaybackState.value
if (playbackState == null || !playbackState.isPlaying) { if (playbackState == null || !playbackState.isPlaying) {

View file

@ -21,9 +21,11 @@ import org.signal.core.util.requireString
import org.signal.core.util.select import org.signal.core.util.select
import org.signal.core.util.update import org.signal.core.util.update
import org.signal.core.util.withinTransaction import org.signal.core.util.withinTransaction
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState.Restrictions import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.calls.log.CallLogRow import org.thoughtcrime.securesms.calls.log.CallLogRow
import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
@ -159,6 +161,30 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
.readToSingleObject { CallLinkDeserializer.deserialize(it) } .readToSingleObject { CallLinkDeserializer.deserialize(it) }
} }
fun getOrCreateCallLinkByRootKey(
callLinkRootKey: CallLinkRootKey
): CallLink {
val roomId = CallLinkRoomId.fromBytes(callLinkRootKey.deriveRoomId())
val callLink = getCallLinkByRoomId(roomId)
return if (callLink == null) {
val link = CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = roomId,
credentials = CallLinkCredentials(
linkKeyBytes = callLinkRootKey.keyBytes,
adminPassBytes = null
),
state = SignalCallLinkState(),
avatarColor = AvatarColorHash.forCallLink(callLinkRootKey.keyBytes)
)
insertCallLink(link)
return getCallLinkByRoomId(roomId)!!
} else {
callLink
}
}
fun getOrCreateCallLinkByRoomId( fun getOrCreateCallLinkByRoomId(
callLinkRoomId: CallLinkRoomId callLinkRoomId: CallLinkRoomId
): CallLink { ): CallLink {

View file

@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash; import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash;
import org.thoughtcrime.securesms.database.CallLinkTable; import org.thoughtcrime.securesms.database.CallLinkTable;
import org.thoughtcrime.securesms.database.CallTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord; import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -347,27 +348,39 @@ public class CommunicationActions {
return; return;
} }
startVideoCall(new ActivityCallContext(activity), rootKey);
}
/**
* Attempts to start a video call for the given call link via root key. This will insert a call link into
* the user's database if one does not already exist.
*
* @param fragment The fragment, which will be used for context and permissions routing.
* @param rootKey The root key of the call link.
*/
public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey) {
startVideoCall(new FragmentCallContext(fragment), rootKey);
}
private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey) {
SimpleTask.run(() -> { SimpleTask.run(() -> {
CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId()); CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId());
if (!SignalDatabase.callLinks().callLinkExists(roomId)) { CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey);
SignalDatabase.callLinks().insertCallLink(new CallLinkTable.CallLink(
RecipientId.UNKNOWN, if (callLink.getState().hasBeenRevoked()) {
roomId, return Optional.<Recipient>empty();
new CallLinkCredentials(
rootKey.getKeyBytes(),
null
),
new SignalCallLinkState("", CallLinkState.Restrictions.UNKNOWN, false, Instant.MIN),
AvatarColorHash.INSTANCE.forCallLink(rootKey.getKeyBytes())
));
} }
return SignalDatabase.recipients().getByCallLinkRoomId(roomId).map(Recipient::resolved); return SignalDatabase.recipients().getByCallLinkRoomId(roomId).map(Recipient::resolved);
}, callLinkRecipient -> { }, callLinkRecipient -> {
if (callLinkRecipient.isEmpty()) { if (callLinkRecipient.isEmpty()) {
// TODO [alex] -- Display a dialog informing them some error happened. new MaterialAlertDialogBuilder(callContext.getContext())
.setTitle(R.string.CommunicationActions_cant_join_call)
.setMessage(R.string.CommunicationActions_this_call_link_is_no_longer_valid)
.setPositiveButton(android.R.string.ok, null)
.show();
} else { } else {
startVideoCall(activity, callLinkRecipient.get()); startVideoCall(callContext, callLinkRecipient.get());
} }
}); });
} }

View file

@ -0,0 +1,20 @@
<?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">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@color/core_white" />
<com.google.android.material.button.MaterialButton
android:id="@+id/join_button"
style="@style/Widget.Signal.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/ConversationItem__join_call" />
</merge>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<org.thoughtcrime.securesms.calls.links.CallLinkJoinButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View file

@ -230,6 +230,12 @@
app:scaleEmojis="true" app:scaleEmojis="true"
tools:text="Mango pickle lorem ipsum" /> tools:text="Mango pickle lorem ipsum" />
<ViewStub
android:id="@+id/conversation_item_join_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_item_join_button" />
<ViewStub <ViewStub
android:id="@+id/conversation_item_call_to_action_stub" android:id="@+id/conversation_item_call_to_action_stub"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -171,6 +171,12 @@
app:scaleEmojis="true" app:scaleEmojis="true"
tools:text="Mango pickle lorem ipsum" /> tools:text="Mango pickle lorem ipsum" />
<ViewStub
android:id="@+id/conversation_item_join_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_item_join_button" />
<org.thoughtcrime.securesms.components.ConversationItemFooter <org.thoughtcrime.securesms.components.ConversationItemFooter
android:id="@+id/conversation_item_footer" android:id="@+id/conversation_item_footer"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -236,6 +236,8 @@
<string name="CommunicationActions_call">Call</string> <string name="CommunicationActions_call">Call</string>
<string name="CommunicationActions_insecure_call">Insecure call</string> <string name="CommunicationActions_insecure_call">Insecure call</string>
<string name="CommunicationActions_carrier_charges_may_apply">Carrier charges may apply. The number you are calling is not registered with Signal. This call will be placed through your mobile carrier, not over the internet.</string> <string name="CommunicationActions_carrier_charges_may_apply">Carrier charges may apply. The number you are calling is not registered with Signal. This call will be placed through your mobile carrier, not over the internet.</string>
<string name="CommunicationActions_cant_join_call">Can\'t join call</string>
<string name="CommunicationActions_this_call_link_is_no_longer_valid">This call link is no longer valid.</string>
<!-- ConfirmIdentityDialog --> <!-- ConfirmIdentityDialog -->
@ -316,6 +318,8 @@
<string name="ConversationItem_cant_download_video_you_will_need_to_send_it_again">Can\'t download video. You will need to send it again.</string> <string name="ConversationItem_cant_download_video_you_will_need_to_send_it_again">Can\'t download video. You will need to send it again.</string>
<!-- Display as the timestamp footer in a message bubble in a conversation when a message has been edited. The timestamp will go from \'11m\' to \'edited 11m\' --> <!-- Display as the timestamp footer in a message bubble in a conversation when a message has been edited. The timestamp will go from \'11m\' to \'edited 11m\' -->
<string name="ConversationItem_edited_timestamp_footer">edited\u2000%1$s</string> <string name="ConversationItem_edited_timestamp_footer">edited\u2000%1$s</string>
<!-- Displayed if the link preview in the conversation item is for a call link call -->
<string name="ConversationItem__join_call">Join call</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->
<string name="ConversationActivity_add_attachment">Add attachment</string> <string name="ConversationActivity_add_attachment">Add attachment</string>