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.Observer;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
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 onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
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 {
private const val ROOT_KEY = "key"
private const val LINK_PREFIX = "https://signal.link/call/#key="
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> {
return Observable.create { emitter ->
@ -51,6 +52,10 @@ object CallLinks {
@JvmStatic
fun parseUrl(url: String): CallLinkRootKey? {
if (!url.startsWith(LINK_PREFIX)) {
return null
}
val parts = url.split("#")
if (parts.size != 2) {
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.StringRes;
import org.signal.ringrtc.CallLinkRootKey;
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.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
@ -202,11 +206,23 @@ public class LinkPreviewView extends FrameLayout {
site.setVisibility(GONE);
}
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, 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 {
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.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
@ -2088,6 +2089,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
GroupDescriptionDialog.show(getChildFragmentManager(), groupName, description, shouldLinkifyWebLinks);
}
@Override
public void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey) {
CommunicationActions.startVideoCall(ConversationFragment.this, callLinkRootKey);
}
@Override
public void onActivatePaymentsClicked() {
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.StringUtil;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.gifts.GiftMessageView;
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.AudioView;
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.revealable.ViewOnceMessageView;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
@ -224,6 +228,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private Stub<LinkPreviewView> linkPreviewStub;
private Stub<BorderlessImageView> stickerStub;
private Stub<ViewOnceMessageView> revealableStub;
private Stub<CallLinkJoinButton> joinCallLinkStub;
private Stub<Button> callToActionStub;
private Stub<GiftMessageView> giftViewStub;
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.stickerStub = new Stub<>(findViewById(R.id.sticker_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.groupSenderHolder = findViewById(R.id.group_sender_holder);
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 (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
if (callToActionStub.resolved()) callToActionStub.get().setVisibility(View.GONE);
paymentViewStub.setVisibility(View.GONE);
revealableStub.get().setMessage((MmsMessageRecord) messageRecord, hasWallpaper);
@ -1123,6 +1130,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
//noinspection ConstantConditions
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)) {
mediaThumbnailStub.require().setVisibility(VISIBLE);
mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content));

View file

@ -37,6 +37,7 @@ object AvatarColorHash {
return forData(seed.toByteArray())
}
@JvmStatic
fun forCallLink(rootKey: ByteArray): AvatarColor {
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.orNull
import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.BlockUnblockDialog
import org.thoughtcrime.securesms.GroupMembersDialog
import org.thoughtcrime.securesms.LoggingFragment
@ -1925,6 +1926,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks)
}
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey)
}
private fun MessageRecord.getAudioUriForLongClick(): Uri? {
val playbackState = getVoiceNoteMediaController().voiceNotePlaybackState.value
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.update
import org.signal.core.util.withinTransaction
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.calls.log.CallLogRow
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@ -159,6 +161,30 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
.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(
callLinkRoomId: CallLinkRoomId
): 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.colors.AvatarColorHash;
import org.thoughtcrime.securesms.database.CallLinkTable;
import org.thoughtcrime.securesms.database.CallTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -347,27 +348,39 @@ public class CommunicationActions {
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(() -> {
CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId());
if (!SignalDatabase.callLinks().callLinkExists(roomId)) {
SignalDatabase.callLinks().insertCallLink(new CallLinkTable.CallLink(
RecipientId.UNKNOWN,
roomId,
new CallLinkCredentials(
rootKey.getKeyBytes(),
null
),
new SignalCallLinkState("", CallLinkState.Restrictions.UNKNOWN, false, Instant.MIN),
AvatarColorHash.INSTANCE.forCallLink(rootKey.getKeyBytes())
));
CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId());
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey);
if (callLink.getState().hasBeenRevoked()) {
return Optional.<Recipient>empty();
}
return SignalDatabase.recipients().getByCallLinkRoomId(roomId).map(Recipient::resolved);
}, callLinkRecipient -> {
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 {
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"
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
android:id="@+id/conversation_item_call_to_action_stub"
android:layout_width="match_parent"

View file

@ -171,6 +171,12 @@
app:scaleEmojis="true"
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
android:id="@+id/conversation_item_footer"
android:layout_width="wrap_content"

View file

@ -236,6 +236,8 @@
<string name="CommunicationActions_call">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_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 -->
@ -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>
<!-- 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>
<!-- Displayed if the link preview in the conversation item is for a call link call -->
<string name="ConversationItem__join_call">Join call</string>
<!-- ConversationActivity -->
<string name="ConversationActivity_add_attachment">Add attachment</string>