Add preview treatment for call links.
This commit is contained in:
parent
4ce05a064c
commit
e3044b8b85
9 changed files with 91 additions and 11 deletions
|
@ -7,7 +7,9 @@ package org.thoughtcrime.securesms.calls.links
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
|
@ -22,6 +24,10 @@ class CallLinkJoinButton @JvmOverloads constructor(
|
|||
|
||||
private val joinButton: MaterialButton = findViewById(R.id.join_button)
|
||||
|
||||
fun setTextColor(@ColorRes textColorResId: Int) {
|
||||
joinButton.setTextColor(ContextCompat.getColor(context, textColorResId))
|
||||
}
|
||||
|
||||
fun setJoinClickListener(onClickListener: OnClickListener) {
|
||||
joinButton.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
|
|
@ -51,6 +51,16 @@ object CallLinks {
|
|||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isCallLink(url: String): Boolean {
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return false
|
||||
}
|
||||
|
||||
return url.split("#").last().startsWith("key=")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseUrl(url: String): CallLinkRootKey? {
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
|
|
|
@ -109,7 +109,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
|||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
|
@ -124,19 +124,19 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
|||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_share_android_24),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked
|
||||
)
|
||||
|
||||
Buttons.MediumTonal(
|
||||
|
|
|
@ -10,7 +10,6 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -247,7 +246,7 @@ private fun CallLinkDetails(
|
|||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
|
||||
modifier = Modifier.clickable(onClick = callback::onEditNameClicked)
|
||||
onClick = callback::onEditNameClicked
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
|
@ -261,14 +260,14 @@ private fun CallLinkDetails(
|
|||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
|
||||
modifier = Modifier.clickable(onClick = callback::onShareClicked)
|
||||
onClick = callback::onShareClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_trash_24),
|
||||
foregroundTint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.clickable(onClick = callback::onDeleteClicked)
|
||||
onClick = callback::onDeleteClicked
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -170,9 +170,13 @@ public class LinkPreviewView extends FrameLayout {
|
|||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
if (!Util.isEmpty(linkPreview.getTitle())) {
|
||||
title.setText(linkPreview.getTitle());
|
||||
title.setVisibility(VISIBLE);
|
||||
} else if (callLinkRootKey != null) {
|
||||
title.setText(R.string.Recipient_signal_call);
|
||||
title.setVisibility(VISIBLE);
|
||||
} else {
|
||||
title.setVisibility(GONE);
|
||||
}
|
||||
|
@ -180,6 +184,9 @@ public class LinkPreviewView extends FrameLayout {
|
|||
if (showDescription && !Util.isEmpty(linkPreview.getDescription())) {
|
||||
description.setText(linkPreview.getDescription());
|
||||
description.setVisibility(VISIBLE);
|
||||
} else if (callLinkRootKey != null) {
|
||||
description.setText(R.string.LinkPreviewView__use_this_link_to_join_a_signal_call);
|
||||
description.setVisibility(VISIBLE);
|
||||
} else {
|
||||
description.setVisibility(GONE);
|
||||
}
|
||||
|
@ -206,7 +213,6 @@ 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);
|
||||
|
|
|
@ -1134,6 +1134,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
if (callLinkRootKey != null) {
|
||||
joinCallLinkStub.setVisibility(View.VISIBLE);
|
||||
joinCallLinkStub.get().setTextColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorOnSurface);
|
||||
joinCallLinkStub.get().setJoinClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onJoinCallLink(callLinkRootKey);
|
||||
|
|
|
@ -19,10 +19,12 @@ import org.signal.libsignal.protocol.InvalidMessageException;
|
|||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
|
@ -42,6 +44,8 @@ import org.thoughtcrime.securesms.net.UserAgentInterceptor;
|
|||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult;
|
||||
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
|
||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
|
@ -64,6 +68,7 @@ import java.util.Optional;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
|
@ -133,6 +138,8 @@ public class LinkPreviewRepository {
|
|||
metadataController = fetchStickerPackLinkPreview(context, url, callback);
|
||||
} else if (GroupInviteLinkUrl.isGroupLink(url)) {
|
||||
metadataController = fetchGroupLinkPreview(context, url, callback);
|
||||
} else if (CallLinks.isCallLink(url)) {
|
||||
metadataController = fetchCallLinkPreview(context, url, callback);
|
||||
} else {
|
||||
metadataController = fetchMetadata(url, metadata -> {
|
||||
if (metadata.isEmpty()) {
|
||||
|
@ -295,6 +302,55 @@ public class LinkPreviewRepository {
|
|||
return () -> Log.i(TAG, "Cancelled sticker pack link preview fetch -- no effect.");
|
||||
}
|
||||
|
||||
private static RequestController fetchCallLinkPreview(@NonNull Context context,
|
||||
@NonNull String callLinkUrl,
|
||||
@NonNull Callback callback) {
|
||||
|
||||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(callLinkUrl);
|
||||
if (callLinkRootKey == null) {
|
||||
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
|
||||
return () -> { };
|
||||
}
|
||||
|
||||
Disposable disposable = ApplicationDependencies.getSignalCallManager()
|
||||
.getCallLinkManager()
|
||||
.readCallLink(new CallLinkCredentials(callLinkRootKey.getKeyBytes(), null))
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
result -> {
|
||||
if (result instanceof ReadCallLinkResult.Success) {
|
||||
ReadCallLinkResult.Success success = (ReadCallLinkResult.Success) result;
|
||||
Log.i(TAG, "Successfully read call link.");
|
||||
|
||||
if (((ReadCallLinkResult.Success) result).getCallLinkState().hasBeenRevoked()) {
|
||||
Log.i(TAG, "Call link has been revoked.");
|
||||
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: thumbnails are generated recv-side using the CallLinkRootKey
|
||||
callback.onSuccess(new LinkPreview(
|
||||
callLinkUrl,
|
||||
success.getCallLinkState().getName(),
|
||||
"",
|
||||
0,
|
||||
Optional.empty()
|
||||
));
|
||||
} else {
|
||||
ReadCallLinkResult.Failure failure = (ReadCallLinkResult.Failure) result;
|
||||
Log.w(TAG, "Failed to read call link: " + failure);
|
||||
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
|
||||
}
|
||||
},
|
||||
error -> {
|
||||
Log.w(TAG, "An error occurred: ", error);
|
||||
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
|
||||
}
|
||||
);
|
||||
|
||||
return disposable::dispose;
|
||||
}
|
||||
|
||||
private static RequestController fetchGroupLinkPreview(@NonNull Context context,
|
||||
@NonNull String groupUrl,
|
||||
@NonNull Callback callback)
|
||||
|
|
|
@ -883,6 +883,8 @@
|
|||
<string name="LinkPreviewView_no_link_preview_available">No link preview available</string>
|
||||
<string name="LinkPreviewView_this_group_link_is_not_active">This group link is not active</string>
|
||||
<string name="LinkPreviewView_domain_date">%1$s · %2$s</string>
|
||||
<!-- Description for Call Link url previews -->
|
||||
<string name="LinkPreviewView__use_this_link_to_join_a_signal_call">Use this link to join a Signal Call</string>
|
||||
|
||||
<!-- LinkPreviewRepository -->
|
||||
<plurals name="LinkPreviewRepository_d_members">
|
||||
|
|
|
@ -112,8 +112,8 @@ object Rows {
|
|||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(defaultPadding())
|
||||
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
||||
.padding(defaultPadding())
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
|
@ -135,8 +135,8 @@ object Rows {
|
|||
text = text,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(defaultPadding())
|
||||
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
||||
.padding(defaultPadding())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue