display vcard view type and allow to import/export contacts as vcard (#3043)

* implement Rpc API for parse_vcard

* allow to create Recipient from VcardContact

* display vcard view-type in incoming/outgoing messages

* fix linter warnings in Recipient class

* properly show vcard view-type in quoted messages

* display fallback avatar in quotes

* allow to attach vcard

* implement basic click listener

* timestamp type got changed from float to int

* set stick translation for "contact"

* share contact as vcard using the new Rpc.makeVcard API

* allow to import contact when clicking vcard
This commit is contained in:
Asiel Díaz Benítez 2024-05-19 22:22:39 +02:00 committed by GitHub
parent 1884f9bc4f
commit c705790d26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 565 additions and 96 deletions

View file

@ -69,6 +69,17 @@
android:padding="8dp" android:padding="8dp"
android:background="@drawable/message_bubble_background_sent_alone"/> android:background="@drawable/message_bubble_background_sent_alone"/>
<org.thoughtcrime.securesms.components.VcardView
android:id="@+id/attachment_vcard"
android:layout_width="230dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:padding="8dp"
android:background="@drawable/message_bubble_background_sent_alone"/>
</org.thoughtcrime.securesms.components.RemovableEditableMediaView> </org.thoughtcrime.securesms.components.RemovableEditableMediaView>
</FrameLayout> </FrameLayout>

View file

@ -153,6 +153,16 @@
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding" android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" /> android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/vcard_view_stub"
android:layout="@layout/conversation_item_vcard"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -131,6 +131,16 @@
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding" android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" /> android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/vcard_view_stub"
android:layout="@layout/conversation_item_vcard"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.VcardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/vcard_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"/>

54
res/layout/vcard_view.xml Normal file
View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:focusable="true">
<org.thoughtcrime.securesms.components.AvatarView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp" />
<LinearLayout android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:paddingRight="16dp"
android:paddingEnd="16dp"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:drawablePadding="5dp"
android:ellipsize="marquee"
android:singleLine="true"
android:fontFamily="sans-serif"
android:textSize="16sp"
tools:text="Frieeeeeeedrich Nieeeeeeeeeetzsche" />
<!-- Attention: Using android:maxLines="1", if the name is an emoji followed by a
long word and the chat is muted, then the long word is not shown at all
(instead of using `…`). That's why we use android:singleLine="true" -->
<TextView android:id="@+id/addr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textDirection="ltr"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="14sp"
android:fontFamily="sans-serif-light"
tools:text="user@example.com" />
</LinearLayout>
</org.thoughtcrime.securesms.contacts.ContactSelectionListItem>

View file

@ -20,6 +20,7 @@ public class DcMsg {
public final static int DC_MSG_FILE = 60; public final static int DC_MSG_FILE = 60;
public final static int DC_MSG_VIDEOCHAT_INVITATION = 70; public final static int DC_MSG_VIDEOCHAT_INVITATION = 70;
public final static int DC_MSG_WEBXDC = 80; public final static int DC_MSG_WEBXDC = 80;
public final static int DC_MSG_VCARD = 90;
public final static int DC_INFO_UNKNOWN = 0; public final static int DC_INFO_UNKNOWN = 0;
public final static int DC_INFO_GROUP_NAME_CHANGED = 2; public final static int DC_INFO_GROUP_NAME_CHANGED = 2;

View file

@ -8,6 +8,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -95,6 +96,20 @@ public class Rpc {
return gson.fromJson(getResult("get_system_info"), mapType.getType()); return gson.fromJson(getResult("get_system_info"), mapType.getType());
} }
public List<VcardContact> parseVcard(String path) throws RpcException {
TypeToken<List<VcardContact>> listType = new TypeToken<List<VcardContact>>(){};
return gson.fromJson(getResult("parse_vcard", path), listType.getType());
}
public String makeVcard(int accountId, int... contacts) throws RpcException {
return gson.fromJson(getResult("make_vcard", accountId, contacts), String.class);
}
public List<Integer> importVcard(int accountId, String path) throws RpcException {
TypeToken<List<Integer>> listType = new TypeToken<List<Integer>>(){};
return gson.fromJson(getResult("import_vcard", accountId, path), listType.getType());
}
public HttpResponse getHttpResponse(int accountId, String url) throws RpcException { public HttpResponse getHttpResponse(int accountId, String url) throws RpcException {
return gson.fromJson(getResult("get_http_response", accountId, url), HttpResponse.class); return gson.fromJson(getResult("get_http_response", accountId, url), HttpResponse.class);
} }

View file

@ -0,0 +1,60 @@
package com.b44t.messenger.rpc;
import android.util.Base64;
public class VcardContact {
// Email address.
private final String addr;
// The contact's name, or the email address if no name was given.
private final String displayName;
// Public PGP key in Base64.
private final String key;
// Profile image in Base64.
private final String profileImage;
// Contact color in HTML color format.
private final String color;
// Last update timestamp.
private final int timestamp;
public VcardContact(String addr, String displayName, String key, String profileImage, String color, int timestamp) {
this.addr = addr;
this.displayName = displayName;
this.key = key;
this.profileImage = profileImage;
this.color = color;
this.timestamp = timestamp;
}
public String getAddr() {
return addr;
}
public String getDisplayName() {
return displayName;
}
public byte[] getKey() {
return key == null? null : Base64.decode(key, Base64.NO_WRAP | Base64.NO_PADDING);
}
public boolean hasProfileImage() {
return profileImage != null;
}
public byte[] getProfileImage() {
return profileImage == null? null : Base64.decode(profileImage, Base64.NO_WRAP | Base64.NO_PADDING);
}
public String getColor() {
return color;
}
public int getTimestamp() {
return timestamp;
}
}

View file

@ -2,26 +2,17 @@ package org.thoughtcrime.securesms;
import android.content.Intent; import android.content.Intent;
import com.b44t.messenger.DcContext;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
public class AttachContactActivity extends ContactSelectionActivity { public class AttachContactActivity extends ContactSelectionActivity {
public static final String NAME_EXTRA = "name_extra"; public static final String CONTACT_ID_EXTRA = "contact_id_extra";
public static final String ADDR_EXTRA = "addr_extra";
@Override @Override
public void onContactSelected(int specialId, String addr) { public void onContactSelected(int specialId, String addr) {
String name = "";
DcContext dcContext = DcHelper.getContext(this);
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId != 0) {
name = dcContext.getContact(contactId).getDisplayName();
}
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra(NAME_EXTRA, name); int contactId = DcHelper.getContext(this).lookupContactIdByAddr(addr);
intent.putExtra(ADDR_EXTRA, addr); intent.putExtra(CONTACT_ID_EXTRA, contactId);
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);
finish(); finish();
} }

View file

@ -71,6 +71,8 @@ import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext; import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg; import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.util.concurrent.ListenableFuture; import com.b44t.messenger.util.concurrent.ListenableFuture;
import com.b44t.messenger.util.concurrent.SettableFuture; import com.b44t.messenger.util.concurrent.SettableFuture;
@ -103,7 +105,6 @@ import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
@ -125,12 +126,7 @@ import org.thoughtcrime.securesms.video.recode.VideoRecoder;
import org.thoughtcrime.securesms.videochat.VideochatUtil; import org.thoughtcrime.securesms.videochat.VideochatUtil;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -193,6 +189,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private ApplicationContext context; private ApplicationContext context;
private Recipient recipient; private Recipient recipient;
private DcContext dcContext; private DcContext dcContext;
private Rpc rpc;
private DcChat dcChat = new DcChat(0, 0); private DcChat dcChat = new DcChat(0, 0);
private int chatId; private int chatId;
private final boolean isSecureText = true; private final boolean isSecureText = true;
@ -204,6 +201,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected void onCreate(Bundle state, boolean ready) { protected void onCreate(Bundle state, boolean ready) {
this.context = ApplicationContext.getInstance(getApplicationContext()); this.context = ApplicationContext.getInstance(getApplicationContext());
this.dcContext = DcHelper.getContext(context); this.dcContext = DcHelper.getContext(context);
this.rpc = DcHelper.getRpc(context);
supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY);
setContentView(R.layout.conversation_activity); setContentView(R.layout.conversation_activity);
@ -978,9 +976,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private void addAttachmentContactInfo(Intent data) { private void addAttachmentContactInfo(Intent data) {
String name = data.getStringExtra(AttachContactActivity.NAME_EXTRA); int contactId = data.getIntExtra(AttachContactActivity.CONTACT_ID_EXTRA, 0);
String mail = data.getStringExtra(AttachContactActivity.ADDR_EXTRA); if (contactId == 0) {
composeText.append(name + "\n" + mail); return;
}
try {
byte[] vcard = rpc.makeVcard(dcContext.getAccountId(), contactId).getBytes();
String mimeType = "application/octet-stream";
setMedia(PersistentBlobProvider.getInstance().create(this, vcard, mimeType, "vcard.vcf"), MediaType.DOCUMENT);
} catch (RpcException e) {
Log.e(TAG, "makeVcard() failed", e);
}
} }
private boolean isMultiUser() { private boolean isMultiUser() {
@ -991,39 +998,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return dcChat.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED; return dcChat.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED;
} }
public static String getRealPathFromAttachment(Context context, Attachment attachment) {
try {
// get file in the blobdir as `<blobdir>/<name>[-<uniqueNumber>].<ext>`
String filename = attachment.getFileName();
String ext = "";
if(filename==null) {
filename = new SimpleDateFormat("yyyy-MM-dd-HH-mm").format(new Date());
ext = "." + MediaUtil.getExtensionFromMimeType(attachment.getContentType());
}
else {
int i = filename.lastIndexOf(".");
if(i>=0) {
ext = filename.substring(i);
filename = filename.substring(0, i);
}
}
String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext);
// copy content to this file
if(path!=null) {
InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream);
}
return path;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
//////// send message or save draft //////// send message or save draft
protected static final int ACTION_SEND_OUT = 1; protected static final int ACTION_SEND_OUT = 1;
@ -1076,7 +1050,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else { } else {
msg = new DcMsg(dcContext, DcMsg.DC_MSG_FILE); msg = new DcMsg(dcContext, DcMsg.DC_MSG_FILE);
} }
String path = getRealPathFromAttachment(this, attachment); String path = attachment.getRealPath(this);
msg.setFile(path, null); msg.setFile(path, null);
} }
} }
@ -1336,7 +1310,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void sendSticker(@NonNull Uri uri, String contentType) { private void sendSticker(@NonNull Uri uri, String contentType) {
Attachment attachment = new UriAttachment(uri, null, contentType, Attachment attachment = new UriAttachment(uri, null, contentType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, null, null, false); AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, null, null, false);
String path = getRealPathFromAttachment(this, attachment); String path = attachment.getRealPath(this);
Optional<QuoteModel> quote = inputPanel.getQuote(); Optional<QuoteModel> quote = inputPanel.getQuote();
inputPanel.clearQuote(); inputPanel.clearQuote();

View file

@ -32,16 +32,19 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DimenRes; import androidx.annotation.DimenRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcChat; import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg; import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.Reactions; import com.b44t.messenger.rpc.Reactions;
import com.b44t.messenger.rpc.RpcException; import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.rpc.VcardContact;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer; import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.AudioView;
@ -51,6 +54,7 @@ import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail; import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.QuoteView; import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView; import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
@ -62,6 +66,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.StickerSlide; import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.reactions.ReactionsConversationView; import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.LongClickMovementMethod; import org.thoughtcrime.securesms.util.LongClickMovementMethod;
@ -71,6 +76,7 @@ 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;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
@ -113,6 +119,7 @@ public class ConversationItem extends BaseConversationItem
private @NonNull Stub<DocumentView> documentViewStub; private @NonNull Stub<DocumentView> documentViewStub;
private @NonNull Stub<WebxdcView> webxdcViewStub; private @NonNull Stub<WebxdcView> webxdcViewStub;
private Stub<BorderlessImageView> stickerStub; private Stub<BorderlessImageView> stickerStub;
private Stub<VcardView> vcardViewStub;
private @Nullable EventListener eventListener; private @Nullable EventListener eventListener;
private int measureCalls; private int measureCalls;
@ -146,6 +153,7 @@ public class ConversationItem extends BaseConversationItem
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub)); this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
this.webxdcViewStub = new Stub<>(findViewById(R.id.webxdc_view_stub)); this.webxdcViewStub = new Stub<>(findViewById(R.id.webxdc_view_stub));
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub)); this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
this.vcardViewStub = new Stub<>(findViewById(R.id.vcard_view_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);
this.container = findViewById(R.id.container); this.container = findViewById(R.id.container);
@ -301,6 +309,11 @@ public class ConversationItem extends BaseConversationItem
webxdcViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); webxdcViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
webxdcViewStub.get().setClickable(batchSelected.isEmpty()); webxdcViewStub.get().setClickable(batchSelected.isEmpty());
} }
if (vcardViewStub.resolved()) {
vcardViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
vcardViewStub.get().setClickable(batchSelected.isEmpty());
}
} }
private void setContentDescription() { private void setContentDescription() {
@ -315,6 +328,8 @@ public class ConversationItem extends BaseConversationItem
desc += documentViewStub.get().getDescription() + "\n"; desc += documentViewStub.get().getDescription() + "\n";
} else if (webxdcViewStub.resolved() && webxdcViewStub.get().getVisibility() == View.VISIBLE) { } else if (webxdcViewStub.resolved() && webxdcViewStub.get().getVisibility() == View.VISIBLE) {
desc += webxdcViewStub.get().getDescription() + "\n"; desc += webxdcViewStub.get().getDescription() + "\n";
} else if (vcardViewStub.resolved() && vcardViewStub.get().getVisibility() == View.VISIBLE) {
desc += vcardViewStub.get().getDescription() + "\n";
} else if (mediaThumbnailStub.resolved() && mediaThumbnailStub.get().getVisibility() == View.VISIBLE) { } else if (mediaThumbnailStub.resolved() && mediaThumbnailStub.get().getVisibility() == View.VISIBLE) {
desc += mediaThumbnailStub.get().getDescription() + "\n"; desc += mediaThumbnailStub.get().getDescription() + "\n";
} else if (stickerStub.resolved() && stickerStub.get().getVisibility() == View.VISIBLE) { } else if (stickerStub.resolved() && stickerStub.get().getVisibility() == View.VISIBLE) {
@ -362,6 +377,10 @@ public class ConversationItem extends BaseConversationItem
return dcMsg.getType()==DcMsg.DC_MSG_WEBXDC; return dcMsg.getType()==DcMsg.DC_MSG_WEBXDC;
} }
private boolean hasVcard(DcMsg dcMsg) {
return dcMsg.getType()==DcMsg.DC_MSG_VCARD;
}
private boolean hasDocument(DcMsg dcMsg) { private boolean hasDocument(DcMsg dcMsg) {
return dcMsg.getType()==DcMsg.DC_MSG_FILE && !dcMsg.isSetupMessage(); return dcMsg.getType()==DcMsg.DC_MSG_FILE && !dcMsg.isSetupMessage();
} }
@ -463,6 +482,7 @@ public class ConversationItem extends BaseConversationItem
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions //noinspection ConstantConditions
int duration = messageRecord.getDuration(); int duration = messageRecord.getDuration();
@ -489,6 +509,7 @@ public class ConversationItem extends BaseConversationItem
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions //noinspection ConstantConditions
documentViewStub.get().setDocument(new DocumentSlide(context, messageRecord)); documentViewStub.get().setDocument(new DocumentSlide(context, messageRecord));
@ -508,6 +529,7 @@ public class ConversationItem extends BaseConversationItem
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
webxdcViewStub.get().setWebxdc(messageRecord, context.getString(R.string.webxdc_app)); webxdcViewStub.get().setWebxdc(messageRecord, context.getString(R.string.webxdc_app));
webxdcViewStub.get().setWebxdcClickListener(new ThumbnailClickListener()); webxdcViewStub.get().setWebxdcClickListener(new ThumbnailClickListener());
@ -520,12 +542,33 @@ public class ConversationItem extends BaseConversationItem
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE); footer.setVisibility(VISIBLE);
} }
else if (hasVcard(messageRecord)) {
vcardViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
vcardViewStub.get().setVcard(glideRequests, new VcardSlide(context, messageRecord), rpc);
vcardViewStub.get().setVcardClickListener(new ThumbnailClickListener());
vcardViewStub.get().setOnLongClickListener(passthroughClickListener);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
vcardViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
}
else if (hasThumbnail(messageRecord)) { else if (hasThumbnail(messageRecord)) {
mediaThumbnailStub.get().setVisibility(View.VISIBLE); mediaThumbnailStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
Slide slide = MediaUtil.getSlideForMsg(context, messageRecord); Slide slide = MediaUtil.getSlideForMsg(context, messageRecord);
@ -566,6 +609,7 @@ public class ConversationItem extends BaseConversationItem
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
bodyBubble.setBackgroundColor(Color.TRANSPARENT); bodyBubble.setBackgroundColor(Color.TRANSPARENT);
@ -587,6 +631,7 @@ public class ConversationItem extends BaseConversationItem
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE);
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@ -839,6 +884,7 @@ public class ConversationItem extends BaseConversationItem
else if (audioViewStub.resolved()) audioViewStub.get().togglePlay(); else if (audioViewStub.resolved()) audioViewStub.get().togglePlay();
else if (documentViewStub.resolved()) documentViewStub.get().performClick(); else if (documentViewStub.resolved()) documentViewStub.get().performClick();
else if (webxdcViewStub.resolved()) webxdcViewStub.get().performClick(); else if (webxdcViewStub.resolved()) webxdcViewStub.get().performClick();
else if (vcardViewStub.resolved()) vcardViewStub.get().performClick();
} }
/// Event handlers /// Event handlers
@ -847,8 +893,37 @@ public class ConversationItem extends BaseConversationItem
public void onClick(final View v, final Slide slide) { public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick(); performClick();
} else if (messageRecord.getType() == DcMsg.DC_MSG_WEBXDC) { } else if (slide.isWebxdcDocument()) {
WebxdcActivity.openWebxdcActivity(context, messageRecord); WebxdcActivity.openWebxdcActivity(context, messageRecord);
} else if (slide.isVcard()) {
try {
String path = slide.asAttachment().getRealPath(context);
VcardContact vcardContact = rpc.parseVcard(path).get(0);
new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.ask_start_chat_with, vcardContact.getDisplayName()))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
try {
List<Integer> contactIds = rpc.importVcard(dcContext.getAccountId(), path);
if (contactIds.size() > 0) {
int chatId = dcContext.createChatByContactId(contactIds.get(0));
if (chatId != 0) {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
context.startActivity(intent);
return;
}
}
} catch (RpcException e) {
Log.e(TAG, "failed to import vCard", e);
}
Toast.makeText(context, context.getResources().getString(R.string.error), Toast.LENGTH_SHORT).show();
})
.setNegativeButton(R.string.cancel, null)
.show();
} catch (RpcException e) {
Log.e(TAG, "failed to parse vCard", e);
Toast.makeText(context, context.getResources().getString(R.string.error), Toast.LENGTH_SHORT).show();
}
} else if (MediaPreviewActivity.isTypeSupported(slide) && slide.getUri() != null) { } else if (MediaPreviewActivity.isTypeSupported(slide) && slide.getUri() != null) {
Intent intent = new Intent(context, MediaPreviewActivity.class); Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

View file

@ -1,9 +1,21 @@
package org.thoughtcrime.securesms.attachments; package org.thoughtcrime.securesms.attachments;
import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
public abstract class Attachment { public abstract class Attachment {
@NonNull @NonNull
@ -88,4 +100,35 @@ public abstract class Attachment {
public int getHeight() { public int getHeight() {
return height; return height;
} }
public String getRealPath(Context context) {
try {
// get file in the blobdir as `<blobdir>/<name>[-<uniqueNumber>].<ext>`
String filename = getFileName();
String ext = "";
if(filename==null) {
filename = new SimpleDateFormat("yyyy-MM-dd-HH-mm").format(new Date());
ext = "." + MediaUtil.getExtensionFromMimeType(getContentType());
}
else {
int i = filename.lastIndexOf(".");
if(i>=0) {
ext = filename.substring(i);
filename = filename.substring(0, i);
}
}
String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext);
// copy content to this file
InputStream inputStream = PartAuthority.getAttachmentStream(context, getDataUri());
OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream);
return path;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
} }

View file

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.attachments; package org.thoughtcrime.securesms.attachments;
import java.io.File; import java.io.File;
import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -35,4 +37,9 @@ public class DcAttachment extends Attachment {
} }
return getDataUri(); return getDataUri();
} }
@Override
public String getRealPath(Context context) {
return dcMsg.getFile();
}
} }

View file

@ -7,6 +7,7 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@ -20,11 +21,14 @@ import androidx.annotation.RequiresApi;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg; import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.rpc.VcardContact;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.json.JSONObject; import org.json.JSONObject;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
@ -185,7 +189,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
} }
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) { private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
List<Slide> thumbnailSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker() || s.isWebxdcDocument()).limit(1).toList(); List<Slide> thumbnailSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker() || s.isWebxdcDocument() || s.isVcard()).limit(1).toList();
List<Slide> audioSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasAudio()).limit(1).toList(); List<Slide> audioSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasAudio()).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList(); List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
@ -206,7 +210,22 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(thumbnailView); .into(thumbnailView);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); Log.e(TAG, "failed to get webxdc icon", e);
thumbnailView.setVisibility(GONE);
}
} else if (thumbnailSlides.get(0).isVcard()) {
try {
VcardContact vcardContact = DcHelper.getRpc(getContext()).parseVcard(quotedMsg.getFile()).get(0);
Recipient recipient = new Recipient(getContext(), vcardContact);
glideRequests.load(recipient.getContactPhoto(getContext()))
.error(recipient.getFallbackAvatarDrawable(getContext(), false))
.centerCrop()
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(thumbnailView);
} catch (RpcException e) {
Log.e(TAG, "failed to parse vCard", e);
thumbnailView.setVisibility(GONE);
} }
} else { } else {
Uri thumbnailUri = thumbnailSlides.get(0).getUri(); Uri thumbnailUri = thumbnailSlides.get(0).getUri();

View file

@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.rpc.Rpc;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.rpc.VcardContact;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.recipients.Recipient;
public class VcardView extends FrameLayout {
private static final String TAG = VcardView.class.getSimpleName();
private final @NonNull AvatarView avatar;
private final @NonNull TextView name;
private final @NonNull TextView address;
private @Nullable SlideClickListener viewListener;
private @Nullable VcardSlide slide;
public VcardView(Context context) {
this(context, null);
}
public VcardView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VcardView(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.vcard_view, this);
this.avatar = findViewById(R.id.avatar);
this.name = findViewById(R.id.name);
this.address = findViewById(R.id.addr);
setOnClickListener(v -> {
if (viewListener != null && slide != null) {
viewListener.onClick(v, slide);
}
});
}
public void setVcardClickListener(@Nullable SlideClickListener listener) {
this.viewListener = listener;
}
public void setVcard(@NonNull GlideRequests glideRequests, final @NonNull VcardSlide slide, final @NonNull Rpc rpc) {
try {
VcardContact vcardContact = rpc.parseVcard(slide.asAttachment().getRealPath(getContext())).get(0);
name.setText(vcardContact.getDisplayName());
address.setText(vcardContact.getAddr());
avatar.setAvatar(glideRequests, new Recipient(getContext(), vcardContact), false);
this.slide = slide;
} catch (RpcException e) {
Log.e(TAG, "failed to parse vCard", e);
}
}
public String getDescription() {
return name.getText() + "\n" + address.getText();
}
}

View file

@ -260,6 +260,7 @@ public class DcHelper {
dcContext.setStockTranslation(177, context.getString(R.string.reaction_by_other)); dcContext.setStockTranslation(177, context.getString(R.string.reaction_by_other));
dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait)); dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait));
dcContext.setStockTranslation(191, context.getString(R.string.secure_join_wait_timeout)); dcContext.setStockTranslation(191, context.getString(R.string.secure_join_wait_timeout));
dcContext.setStockTranslation(200, context.getString(R.string.contact));
} }
public static File getImexDir() { public static File getImexDir() {

View file

@ -24,17 +24,21 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
@Override @Override
public Drawable asDrawable(Context context, int color) { public Drawable asDrawable(Context context, int color) {
return asDrawable(context, color, true);
}
public Drawable asDrawable(Context context, int color, boolean roundShape) {
int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
return TextDrawable.builder() TextDrawable.IShapeBuilder builder = TextDrawable.builder()
.beginConfig() .beginConfig()
.width(targetSize) .width(targetSize)
.height(targetSize) .height(targetSize)
.textColor(Color.WHITE) .textColor(Color.WHITE)
.bold() .bold()
.toUpperCase() .toUpperCase()
.endConfig() .endConfig();
.buildRound(getCharacter(name), color); return roundShape? builder.buildRound(getCharacter(name), color) : builder.buildRect(getCharacter(name), color);
} }
private String getCharacter(String name) { private String getCharacter(String name) {

View file

@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.contacts.avatars;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.rpc.VcardContact;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
public class VcardContactPhoto implements ContactPhoto {
private final VcardContact vContact;
public VcardContactPhoto(VcardContact vContact) {
this.vContact = vContact;
}
@Override
public InputStream openInputStream(Context context) throws IOException {
byte[] blob = vContact.getProfileImage();
return (blob == null)? null : new ByteArrayInputStream(blob);
}
@Override
public @Nullable Uri getUri(@NonNull Context context) {
return null;
}
@Override
public boolean isProfilePhoto() {
return true;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(vContact.getAddr().getBytes());
messageDigest.update(ByteBuffer.allocate(4).putFloat(vContact.getTimestamp()).array());
}
}

View file

@ -41,12 +41,12 @@ import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcContext; import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg; import com.b44t.messenger.DcMsg;
import com.b44t.messenger.rpc.RpcException;
import com.b44t.messenger.util.concurrent.ListenableFuture; import com.b44t.messenger.util.concurrent.ListenableFuture;
import com.b44t.messenger.util.concurrent.ListenableFuture.Listener; import com.b44t.messenger.util.concurrent.ListenableFuture.Listener;
import com.b44t.messenger.util.concurrent.SettableFuture; import com.b44t.messenger.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareLocationDialog; import org.thoughtcrime.securesms.ShareLocationDialog;
@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView; import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -93,6 +94,7 @@ public class AttachmentManager {
private AudioView audioView; private AudioView audioView;
private DocumentView documentView; private DocumentView documentView;
private WebxdcView webxdcView; private WebxdcView webxdcView;
private VcardView vcardView;
//private SignalMapView mapView; //private SignalMapView mapView;
private final @NonNull List<Uri> garbage = new LinkedList<>(); private final @NonNull List<Uri> garbage = new LinkedList<>();
@ -116,6 +118,7 @@ public class AttachmentManager {
this.audioView = ViewUtil.findById(root, R.id.attachment_audio); this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
this.documentView = ViewUtil.findById(root, R.id.attachment_document); this.documentView = ViewUtil.findById(root, R.id.attachment_document);
this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc); this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc);
this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard);
//this.mapView = ViewUtil.findById(root, R.id.attachment_location); //this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view); this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
@ -126,6 +129,7 @@ public class AttachmentManager {
audioView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); audioView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
documentView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); documentView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
webxdcView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); webxdcView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
vcardView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY);
} }
} }
@ -308,6 +312,9 @@ public class AttachmentManager {
audioView.setAudio((AudioSlide) slide, 0); audioView.setAudio((AudioSlide) slide, 0);
removableMediaView.display(audioView, false); removableMediaView.display(audioView, false);
result.set(true); result.set(true);
} else if (slide.isVcard()) {
vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context));
removableMediaView.display(vcardView, false);
} else if (slide.hasDocument()) { } else if (slide.hasDocument()) {
if (slide.isWebxdcDocument()) { if (slide.isWebxdcDocument()) {
DcMsg instance = msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId); DcMsg instance = msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId);
@ -685,13 +692,25 @@ public class AttachmentManager {
DcContext dcContext = DcHelper.getContext(context); DcContext dcContext = DcHelper.getContext(context);
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC); DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC);
Attachment attachment = new UriAttachment(uri, null, MediaUtil.WEBXDC, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, fileName, null, false); Attachment attachment = new UriAttachment(uri, null, MediaUtil.WEBXDC, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, fileName, null, false);
String path = ConversationActivity.getRealPathFromAttachment(context, attachment); String path = attachment.getRealPath(context);
msg.setFile(path, MediaUtil.WEBXDC); msg.setFile(path, MediaUtil.WEBXDC);
dcContext.setDraft(chatId, msg); dcContext.setDraft(chatId, msg);
return new DocumentSlide(context, msg); return new DocumentSlide(context, msg);
} else {
return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
} }
if (mimeType.equals(MediaUtil.VCARD) || (fileName != null && (fileName.endsWith(".vcf") || fileName.endsWith(".vcard")))) {
VcardSlide slide = new VcardSlide(context, uri, dataSize, fileName);
String path = slide.asAttachment().getRealPath(context);
try {
if (DcHelper.getRpc(context).parseVcard(path).size() == 1) {
return slide;
}
} catch (RpcException e) {
e.printStackTrace();
}
}
return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
default: throw new AssertionError("unrecognized enum"); default: throw new AssertionError("unrecognized enum");
} }
} }

View file

@ -98,6 +98,10 @@ public abstract class Slide {
return false; return false;
} }
public boolean isVcard() {
return false;
}
public boolean hasLocation() { public boolean hasLocation() {
return false; return false;
} }

View file

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.DcMsg;
public class VcardSlide extends DocumentSlide {
public VcardSlide(Context context, DcMsg dcMsg) {
super(context, dcMsg);
}
public VcardSlide(@NonNull Context context, @NonNull Uri uri, long size, @Nullable String fileName) {
super(context, uri, "text/vcard", size, fileName);
}
@Override
public boolean isVcard() {
return true;
}
}

View file

@ -21,13 +21,15 @@ import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.b44t.messenger.DcChat; import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext; import com.b44t.messenger.DcContext;
import com.b44t.messenger.rpc.VcardContact;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
@ -37,15 +39,14 @@ import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.LocalFileContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.LocalFileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.VcardContactPhoto;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.util.Hash; import org.thoughtcrime.securesms.util.Hash;
import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.WeakHashMap; import java.util.WeakHashMap;
@ -65,6 +66,7 @@ public class Recipient {
private final @Nullable DcChat dcChat; private final @Nullable DcChat dcChat;
private @Nullable DcContact dcContact; private @Nullable DcContact dcContact;
private final @Nullable VcardContact vContact;
public static @NonNull Recipient fromChat(@NonNull Context context, int dcMsgId) { public static @NonNull Recipient fromChat(@NonNull Context context, int dcMsgId) {
DcContext dcContext = DcHelper.getContext(context); DcContext dcContext = DcHelper.getContext(context);
@ -90,21 +92,26 @@ public class Recipient {
} }
public Recipient(@NonNull Context context, @NonNull DcChat dcChat) { public Recipient(@NonNull Context context, @NonNull DcChat dcChat) {
this(context, dcChat, null, null); this(context, dcChat, null, null, null);
}
public Recipient(@NonNull Context context, @NonNull VcardContact vContact) {
this(context, null, null, null, vContact);
} }
public Recipient(@NonNull Context context, @NonNull DcContact dcContact) { public Recipient(@NonNull Context context, @NonNull DcContact dcContact) {
this(context, null, dcContact, null); this(context, null, dcContact, null, null);
} }
public Recipient(@NonNull Context context, @NonNull DcContact dcContact, @NonNull String profileName) { public Recipient(@NonNull Context context, @NonNull DcContact dcContact, @NonNull String profileName) {
this(context, null, dcContact, profileName); this(context, null, dcContact, profileName, null);
} }
private Recipient(@NonNull Context context, @Nullable DcChat dcChat, @Nullable DcContact dcContact, @Nullable String profileName) { private Recipient(@NonNull Context context, @Nullable DcChat dcChat, @Nullable DcContact dcContact, @Nullable String profileName, @Nullable VcardContact vContact) {
this.dcChat = dcChat; this.dcChat = dcChat;
this.dcContact = dcContact; this.dcContact = dcContact;
this.profileName = profileName; this.profileName = profileName;
this.vContact = vContact;
this.contactUri = null; this.contactUri = null;
this.systemContactPhoto = null; this.systemContactPhoto = null;
this.customLabel = null; this.customLabel = null;
@ -141,6 +148,9 @@ public class Recipient {
else if(dcContact!=null) { else if(dcContact!=null) {
return dcContact.getDisplayName(); return dcContact.getDisplayName();
} }
else if(vContact!=null) {
return vContact.getDisplayName();
}
return ""; return "";
} }
@ -168,18 +178,6 @@ public class Recipient {
return dcChat!=null && dcChat.isMultiUser(); return dcChat!=null && dcChat.isMultiUser();
} }
public @NonNull List<Recipient> loadParticipants(Context context) {
List<Recipient> participants = new ArrayList<>();
if (dcChat!=null) {
DcContext dcContext = DcHelper.getAccounts(context).getAccount(dcChat.getAccountId());
int[] contactIds = dcContext.getChatContacts(dcChat.getId());
for (int contactId : contactIds) {
participants.add(new Recipient(context, dcContext.getContact(contactId)));
}
}
return participants;
}
public synchronized void addListener(RecipientModifiedListener listener) { public synchronized void addListener(RecipientModifiedListener listener) {
listeners.add(listener); listeners.add(listener);
} }
@ -200,15 +198,21 @@ public class Recipient {
else if(dcContact!=null) { else if(dcContact!=null) {
rgb = dcContact.getColor(); rgb = dcContact.getColor();
} }
int argb = Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb)); else if(vContact!=null) {
return argb; rgb = Color.parseColor(vContact.getColor());
}
return Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb));
} }
public synchronized @NonNull Drawable getFallbackAvatarDrawable(Context context) { public synchronized @NonNull Drawable getFallbackAvatarDrawable(Context context) {
return getFallbackContactPhoto().asDrawable(context, getFallbackAvatarColor()); return getFallbackAvatarDrawable(context, true);
} }
public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() { public synchronized @NonNull Drawable getFallbackAvatarDrawable(Context context, boolean roundShape) {
return getFallbackContactPhoto().asDrawable(context, getFallbackAvatarColor(), roundShape);
}
public synchronized @NonNull GeneratedContactPhoto getFallbackContactPhoto() {
String name = getName(); String name = getName();
if (!TextUtils.isEmpty(profileName)) return new GeneratedContactPhoto(profileName); if (!TextUtils.isEmpty(profileName)) return new GeneratedContactPhoto(profileName);
else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name); else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name);
@ -231,6 +235,10 @@ public class Recipient {
} }
} }
if (vContact!=null && vContact.hasProfileImage()) {
return new VcardContactPhoto(vContact);
}
if (systemContactPhoto != null) { if (systemContactPhoto != null) {
return new SystemContactPhoto(address, systemContactPhoto, 0); return new SystemContactPhoto(address, systemContactPhoto, 0);
} }
@ -262,7 +270,7 @@ public class Recipient {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || !(o instanceof Recipient)) return false; if (!(o instanceof Recipient)) return false;
Recipient that = (Recipient) o; Recipient that = (Recipient) o;
@ -290,6 +298,7 @@ public class Recipient {
return dcChat!=null? dcChat : new DcChat(0, 0); return dcChat!=null? dcChat : new DcChat(0, 0);
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Recipient{" + return "Recipient{" +

View file

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.StickerSlide; import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
@ -48,6 +49,7 @@ public class MediaUtil {
public static final String VIDEO_UNSPECIFIED = "video/*"; public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String OCTET = "application/octet-stream"; public static final String OCTET = "application/octet-stream";
public static final String WEBXDC = "application/webxdc+zip"; public static final String WEBXDC = "application/webxdc+zip";
public static final String VCARD = "text/vcard";
public static Slide getSlideForMsg(Context context, DcMsg dcMsg) { public static Slide getSlideForMsg(Context context, DcMsg dcMsg) {
@ -63,6 +65,8 @@ public class MediaUtil {
} else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO } else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO
|| dcMsg.getType() == DcMsg.DC_MSG_VOICE) { || dcMsg.getType() == DcMsg.DC_MSG_VOICE) {
slide = new AudioSlide(context, dcMsg); slide = new AudioSlide(context, dcMsg);
} else if (dcMsg.getType() == DcMsg.DC_MSG_VCARD) {
slide = new VcardSlide(context, dcMsg);
} else if (dcMsg.getType() == DcMsg.DC_MSG_FILE } else if (dcMsg.getType() == DcMsg.DC_MSG_FILE
|| dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) { || dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) {
slide = new DocumentSlide(context, dcMsg); slide = new DocumentSlide(context, dcMsg);
@ -301,6 +305,8 @@ public class MediaUtil {
return "webp"; return "webp";
case WEBXDC: case WEBXDC:
return "xdc"; return "xdc";
case VCARD:
return "vcf";
} }
return null; return null;
} }