CallLink treatment for ConversationItem.
This commit is contained in:
parent
8f96abb41e
commit
93df01e266
15 changed files with 180 additions and 15 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -37,6 +37,7 @@ object AvatarColorHash {
|
|||
return forData(seed.toByteArray())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forCallLink(rootKey: ByteArray): AvatarColor {
|
||||
return forIndex(rootKey.first().toInt())
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
20
app/src/main/res/layout/call_link_join_button.xml
Normal file
20
app/src/main/res/layout/call_link_join_button.xml
Normal 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>
|
|
@ -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" />
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue