Parse invitation link from search (#4432)

This commit is contained in:
wchen342
2026-05-15 12:10:32 -04:00
committed by GitHub
parent 4d9f8dd244
commit 55649b0c4b
8 changed files with 249 additions and 36 deletions
@@ -601,6 +601,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
}
}
public void handleQrFromSearch(String rawQrString) {
qrData = rawQrString;
new QrCodeHandler(this)
.handleQrData(
rawQrString, SecurejoinSource.Scan, SecurejoinUiPath.QrIcon, relayLockLauncher);
}
private void handleResetRelaying() {
resetRelayingMessageContent(this);
refreshTitle();
@@ -44,9 +44,11 @@ import org.thoughtcrime.securesms.components.AvatarView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.search.QrInviteData;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -216,6 +218,40 @@ public class ConversationListItem extends RelativeLayout
avatar.setSeenRecently(false);
}
public void bind(@NonNull QrInviteData inviteData, @NonNull GlideRequests glideRequests) {
this.selectedThreads = Collections.emptySet();
fromView.setText(inviteData.getDisplayTitle());
fromView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
subjectView.setVisibility(VISIBLE);
subjectView.setText(inviteData.getDisplaySubtitle());
subjectView.setTypeface(LIGHT_TYPEFACE);
subjectView.setTextColor(
ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color));
dateView.setText("");
dateView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
archivedBadgeView.setVisibility(GONE);
requestBadgeView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
deliveryStatusIndicator.setNone();
setBatchState(false);
if (inviteData.getContactId() > 0) {
DcContext dcContext = DcHelper.getContext(getContext());
DcContact contact = dcContext.getContact(inviteData.getContactId());
Recipient recipient = new Recipient(getContext(), contact);
avatar.setAvatar(glideRequests, recipient, false);
avatar.setSeenRecently(contact.wasSeenRecently());
} else {
avatar.setImageDrawable(
new GeneratedContactPhoto("+")
.asDrawable(getContext(), ThemeUtil.getDummyContactColor(getContext())));
avatar.setSeenRecently(false);
}
}
@Override
public void unbind() {}
@@ -0,0 +1,91 @@
package org.thoughtcrime.securesms.search;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import org.thoughtcrime.securesms.R;
public class QrInviteData {
private final String displayTitle;
private final String displaySubtitle;
private final String rawQrString;
private final int contactId;
private QrInviteData(
@NonNull String displayTitle,
@NonNull String displaySubtitle,
@NonNull String rawQrString,
int contactId) {
this.displayTitle = displayTitle;
this.displaySubtitle = displaySubtitle;
this.rawQrString = rawQrString;
this.contactId = contactId;
}
@Nullable
public static QrInviteData from(
@NonNull Context context,
@NonNull DcContext dcContext,
@NonNull DcLot qrParsed,
@NonNull String rawQrString) {
int state = qrParsed.getState();
String title;
String subtitle;
int contactId = 0;
switch (state) {
case DcContext.DC_QR_ASK_VERIFYCONTACT:
case DcContext.DC_QR_FPR_OK:
case DcContext.DC_QR_ADDR:
contactId = qrParsed.getId();
DcContact contact = dcContext.getContact(contactId);
title = contact.getDisplayName();
subtitle = context.getString(R.string.start_chat);
break;
case DcContext.DC_QR_ASK_VERIFYGROUP:
title = qrParsed.getText1();
subtitle = context.getString(R.string.join_group);
break;
case DcContext.DC_QR_ASK_JOIN_BROADCAST:
title = qrParsed.getText1();
subtitle = context.getString(R.string.join_channel);
break;
case DcContext.DC_QR_ACCOUNT:
case DcContext.DC_QR_LOGIN:
title = qrParsed.getText1();
subtitle = context.getString(R.string.add_transport);
break;
case DcContext.DC_QR_PROXY:
title = qrParsed.getText1();
subtitle = context.getString(R.string.proxy_use_proxy);
break;
default:
return null;
}
return new QrInviteData(title, subtitle, rawQrString, contactId);
}
@NonNull
public String getDisplayTitle() {
return displayTitle;
}
@NonNull
public String getDisplaySubtitle() {
return displaySubtitle;
}
@NonNull
public String getRawQrString() {
return rawQrString;
}
public int getContactId() {
return contactId;
}
}
@@ -200,6 +200,14 @@ public class SearchFragment extends BaseConversationListFragment
}
}
@Override
public void onInvitationClicked(@NonNull String rawQrString) {
ConversationListActivity conversationList = (ConversationListActivity) getActivity();
if (conversationList != null) {
conversationList.handleQrFromSearch(rawQrString);
}
}
public void updateSearchQuery(@NonNull String query) {
if (viewModel != null) {
viewModel.updateQuery(query);
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
class SearchListAdapter
extends BaseConversationListAdapter<SearchListAdapter.SearchResultViewHolder>
implements StickyHeaderDecoration.StickyHeaderAdapter<SearchListAdapter.HeaderViewHolder> {
private static final int TYPE_QR_INVITE = 0;
private static final int TYPE_CHATS = 1;
private static final int TYPE_CONTACTS = 2;
private static final int TYPE_MESSAGES = 3;
@@ -57,6 +58,14 @@ class SearchListAdapter
@Override
public void onBindViewHolder(@NonNull SearchResultViewHolder holder, int position) {
if (isQrInvitePosition(position)) {
QrInviteData inviteData = searchResult.getQrInviteData();
if (inviteData != null) {
holder.bind(inviteData, glideRequests, eventListener);
}
return;
}
DcChatlist.Item conversationResult = getConversationResult(position);
if (conversationResult != null) {
@@ -97,7 +106,9 @@ class SearchListAdapter
@Override
public long getHeaderId(int position) {
if (getConversationResult(position) != null) {
if (isQrInvitePosition(position)) {
return TYPE_QR_INVITE;
} else if (getConversationResult(position) != null) {
return TYPE_CHATS;
} else if (getContactResult(position) != null) {
return TYPE_CONTACTS;
@@ -116,33 +127,39 @@ class SearchListAdapter
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
int headerType = (int) getHeaderId(position);
int textId = R.plurals.n_messages;
int count = 1;
boolean maybeLimitedTo1000 = false;
String title;
switch (headerType) {
case TYPE_CHATS:
textId = R.plurals.n_chats;
count = searchResult.getChats().getCnt();
break;
case TYPE_CONTACTS:
textId = R.plurals.n_contacts;
count = searchResult.getContacts().length;
break;
case TYPE_MESSAGES:
textId = R.plurals.n_messages;
count = searchResult.getMessages().length;
maybeLimitedTo1000 =
count == 1000; // a count of 1000 results may be limited, see documentation of
// dc_search_msgs()
break;
}
if (headerType == TYPE_QR_INVITE) {
title = context.getString(R.string.link);
} else {
int textId = R.plurals.n_messages;
int count = 1;
boolean maybeLimitedTo1000 = false;
String title = context.getResources().getQuantityString(textId, count, count);
if (maybeLimitedTo1000) {
title =
title.replace(
"000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000"
switch (headerType) {
case TYPE_CHATS:
textId = R.plurals.n_chats;
count = searchResult.getChats().getCnt();
break;
case TYPE_CONTACTS:
textId = R.plurals.n_contacts;
count = searchResult.getContacts().length;
break;
case TYPE_MESSAGES:
textId = R.plurals.n_messages;
count = searchResult.getMessages().length;
maybeLimitedTo1000 =
count == 1000; // a count of 1000 results may be limited, see documentation of
// dc_search_msgs()
break;
}
title = context.getResources().getQuantityString(textId, count, count);
if (maybeLimitedTo1000) {
title =
title.replace(
"000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000"
}
}
viewHolder.bind(title);
}
@@ -160,10 +177,19 @@ class SearchListAdapter
notifyDataSetChanged();
}
private int getQrInviteCount() {
return searchResult.getQrInviteData() != null ? 1 : 0;
}
private boolean isQrInvitePosition(int position) {
return position == 0 && searchResult.getQrInviteData() != null;
}
@Nullable
private DcChatlist.Item getConversationResult(int position) {
if (position < searchResult.getChats().getCnt()) {
return searchResult.getChats().getItem(position);
int offset = position - getQrInviteCount();
if (offset >= 0 && offset < searchResult.getChats().getCnt()) {
return searchResult.getChats().getItem(offset);
}
return null;
}
@@ -185,7 +211,7 @@ class SearchListAdapter
}
private int getFirstContactIndex() {
return searchResult.getChats().getCnt();
return getQrInviteCount() + searchResult.getChats().getCnt();
}
private int getFirstMessageIndex() {
@@ -200,6 +226,8 @@ class SearchListAdapter
void onContactClicked(@NonNull DcContact contact);
void onMessageClicked(@NonNull DcMsg message);
void onInvitationClicked(@NonNull String rawQrString);
}
static class SearchResultViewHolder extends RecyclerView.ViewHolder {
@@ -257,9 +285,20 @@ class SearchListAdapter
root.setOnClickListener(view -> eventListener.onMessageClicked(messageResult));
}
void bind(
@NonNull QrInviteData inviteData,
@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener) {
root.bind(inviteData, glideRequests);
root.setOnClickListener(
view -> eventListener.onInvitationClicked(inviteData.getRawQrString()));
root.setOnLongClickListener(null);
}
void recycle() {
root.unbind();
root.setOnClickListener(null);
root.setOnLongClickListener(null);
}
}
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.b44t.messenger.DcChatlist;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.util.Util;
@@ -18,13 +19,15 @@ class SearchViewModel extends ViewModel {
private static final String TAG = "SearchViewModel";
private final ObservingLiveData searchResult;
private String lastQuery;
private final Context appContext;
private final DcContext dcContext;
private boolean forwarding = false;
private boolean inBgSearch;
private boolean needsAnotherBgSearch;
SearchViewModel(@NonNull Context context) {
this.dcContext = DcHelper.getContext(context.getApplicationContext());
this.appContext = context.getApplicationContext();
this.dcContext = DcHelper.getContext(appContext);
this.searchResult = new ObservingLiveData();
}
@@ -73,6 +76,12 @@ class SearchViewModel extends ViewModel {
return;
}
QrInviteData qrInviteData = null;
if (!forwarding && query.contains(":")) {
DcLot qrParsed = dcContext.checkQr(query);
qrInviteData = QrInviteData.from(appContext, dcContext, qrParsed, query);
}
// #1 search for chats
long startMs = System.currentTimeMillis();
DcChatlist conversations =
@@ -83,7 +92,8 @@ class SearchViewModel extends ViewModel {
// #2 search for contacts
if (!query.equals(lastQuery) && overallCnt > 0) {
Log.i(TAG, "... skipping getContacts() and searchMsgs(), more recent search pending");
callback.onResult(new SearchResult(query, new int[0], conversations, new int[0]));
callback.onResult(
new SearchResult(query, new int[0], conversations, new int[0], qrInviteData));
return;
}
@@ -95,19 +105,19 @@ class SearchViewModel extends ViewModel {
// #3 search for messages
if (forwarding) {
Log.i(TAG, "... searchMsgs() disabled by caller");
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
return;
}
if (query.length() <= 1) {
Log.i(TAG, "... skipping searchMsgs(), string too short");
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
return;
}
if (!query.equals(lastQuery) && overallCnt > 0) {
Log.i(TAG, "... skipping searchMsgs(), more recent search pending");
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
return;
}
@@ -115,7 +125,7 @@ class SearchViewModel extends ViewModel {
int[] messages = dcContext.searchMsgs(0, query);
Log.i(TAG, "⏰ searchMsgs(" + query + "): " + (System.currentTimeMillis() - startMs) + "ms");
callback.onResult(new SearchResult(query, contacts, conversations, messages));
callback.onResult(new SearchResult(query, contacts, conversations, messages, qrInviteData));
}
@NonNull
@@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.search.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.DcChatlist;
import org.thoughtcrime.securesms.search.QrInviteData;
/**
* Represents an all-encompassing search result that can contain various result for different
@@ -16,16 +18,27 @@ public class SearchResult {
private final int[] contacts;
private final DcChatlist conversations;
private final int[] messages;
private final QrInviteData qrInviteData;
public SearchResult(
@NonNull String query,
@NonNull int[] contacts,
@NonNull DcChatlist conversations,
@NonNull int[] messages) {
this(query, contacts, conversations, messages, null);
}
public SearchResult(
@NonNull String query,
@NonNull int[] contacts,
@NonNull DcChatlist conversations,
@NonNull int[] messages,
@Nullable QrInviteData qrInviteData) {
this.query = query;
this.contacts = contacts;
this.conversations = conversations;
this.messages = messages;
this.qrInviteData = qrInviteData;
}
public int[] getContacts() {
@@ -44,8 +57,16 @@ public class SearchResult {
return query;
}
@Nullable
public QrInviteData getQrInviteData() {
return qrInviteData;
}
public int size() {
return contacts.length + conversations.getCnt() + messages.length;
return (qrInviteData != null ? 1 : 0)
+ contacts.length
+ conversations.getCnt()
+ messages.length;
}
public boolean isEmpty() {