Updated reply-to UI.

All UI components are now properly styled and functioning according to
spec.
pull/1/head
Greyson Parrelli 6 years ago committed by Moxie Marlinspike
parent d567534609
commit fa99e8f0d0

@ -75,7 +75,7 @@ dependencies {
compile('org.whispersystems:libpastelog:1.1.2') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
compile 'org.whispersystems:signal-service-android:2.7.3'
compile 'org.whispersystems:signal-service-android:2.7.5'
compile 'org.whispersystems:webrtc-android:M64'
compile "me.leolin:ShortcutBadger:1.1.16"
@ -164,7 +164,7 @@ dependencyVerification {
'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:fe56b4db9ec743c8b565e3e4caa9228fafe132dc0bf82000d6e359b97a81177c',
'org.whispersystems:signal-service-android:dd0c21b37b239ac9c3eaf0b290791a3708817daa13e82e24b0544631f948d8d3',
'org.whispersystems:signal-service-android:e0a3d55b21c1db483818ed459c500eba96dfb839e70d95dca4d8d4c1a7cd816b',
'org.whispersystems:webrtc-android:ed297e8b795dad9658cf306c2aa0f7d296c65f0997a2ac4353fd0157910acc12',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@ -203,7 +203,7 @@ dependencyVerification {
'com.github.bumptech.glide:gifdecoder:59ccf3bb0cec11dab4b857382cbe0b171111b6fc62bf141adce4e1180889af15',
'com.android.support:support-annotations:af05330d997eb92a066534dbe0a3ea24347d26d7001221092113ae02a8f233da',
'org.whispersystems:signal-protocol-android:5b8acded7f2a40178eb90ab8e8cbfec89d170d91b3ff5e78487d1098df6185a1',
'org.whispersystems:signal-service-java:6654e52469b77db5c720de9557abe41bf99a9034c170c8a09e00bd2487c86430',
'org.whispersystems:signal-service-java:7b4c34e3a346a236caebd5b81fb2985ed3c91a9974a8a8ddd36b6e1b8ae9350a',
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" android:exitFadeDuration="1000">
<item android:drawable="@color/textsecure_primary_alpha33" android:state_selected="true" />
<item android:drawable="@color/signal_primary_alpha_focus" android:state_focused="true" />
</selector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<solid android:color="#99ffffff" />
</shape>

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray5"/>
<stroke android:color="@color/gray10" android:width="1dp"/>
<corners android:radius="5dp" />
<stroke android:color="@color/grey_400_transparent" android:width="@dimen/quote_outline_width"/>
<corners android:radius="@dimen/quote_corner_radius" />
</shape>

@ -34,7 +34,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:quote_dismissable="true"
app:message_type="preview"
tools:visibility="visible"/>
<LinearLayout android:layout_width="match_parent"

@ -79,8 +79,9 @@
android:id="@+id/quote_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:visibility="gone"
app:quote_dismissable="false"
app:message_type="incoming"
tools:visibility="visible"/>
<ViewStub

@ -42,8 +42,9 @@
android:id="@+id/quote_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:visibility="gone"
app:quote_dismissable="false"
app:message_type="outgoing"
tools:visibility="visible"/>
<ViewStub

@ -1,101 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/quote_container"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_margin="3dp"
android:background="@drawable/quote_background"
tools:visibility="visible"
tools:parentTag="android.widget.LinearLayout">
<ImageView android:id="@+id/quote_bar"
android:layout_width="5dp"
android:layout_height="match_parent"
android:src="@drawable/quote_bar"
tools:tint="@color/purple_400"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:paddingBottom="10dp">
<TextView android:id="@+id/quote_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:maxLines="1"
tools:textColor="@color/purple_400"
tools:text="Riya"/>
<LinearLayout android:id="@+id/media_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<ImageView android:id="@+id/media_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/gray50"
android:src="@drawable/ic_insert_photo_white_18dp"/>
<TextView android:id="@+id/media_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:textSize="11sp"
tools:text="Photo"/>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/quote_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
android:layout_margin="3dp"
tools:visibility="visible">
</LinearLayout>
<FrameLayout
android:id="@+id/quote_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/quote_background">
<TextView android:id="@+id/quote_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:maxLines="3"
android:ellipsize="end"
tools:text="Short text."
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
</LinearLayout>
<ImageView
android:id="@+id/quote_bar"
android:layout_width="@dimen/quote_corner_radius"
android:layout_height="match_parent"
android:src="@color/white"
tools:tint="@color/purple_400" />
<FrameLayout android:layout_width="wrap_content"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="vertical"
android:layout_weight="1">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/quote_attachment"
android:layout_width="80dp"
android:layout_height="match_parent"
app:riv_corner_radius_top_right="5dp"
app:riv_corner_radius_bottom_right="5dp"
android:scaleType="centerCrop"
<TextView
android:id="@+id/quote_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:maxLines="1"
tools:text="Peter Parker"
tools:textColor="@color/purple_400" />
<TextView
android:id="@+id/media_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:paddingTop="4dp"
android:textSize="12sp"
android:textStyle="italic"
android:visibility="gone"
tools:text="Photo"
tools:visibility="visible" />
<TextView
android:id="@+id/quote_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="3"
tools:text="With great power comes great responsibility."
tools:visibility="visible" />
</LinearLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/quote_attachment"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
android:visibility="gone"
tools:visibility="gone" />
<ImageView
android:id="@+id/quote_video_overlay"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:padding="18dp"
android:src="@drawable/ic_play_arrow_white_24dp"
android:background="@color/transparent_black_30"
android:visibility="gone"
tools:visibility="gone"/>
</FrameLayout>
<FrameLayout
android:id="@+id/quote_attachment_icon_container"
android:layout_width="60dp"
android:layout_height="60dp"
android:visibility="gone"
tools:src="@drawable/surfwalk2"
tools:visibility="visible"/>
<ImageView android:id="@+id/quote_dismiss"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:src="@drawable/ic_close_white_18dp"
android:tint="@color/gray70"
android:background="@drawable/circle_alpha"
android:layout_marginTop="5dp"
android:layout_marginRight="5dp"
android:layout_marginEnd="5dp"/>
</FrameLayout>
tools:visibility="visible">
<ImageView
android:id="@+id/quote_attachment_icon_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:src="@drawable/circle_tintable" />
<ImageView
android:id="@+id/quote_attachment_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp"
tools:src="@drawable/ic_insert_drive_file_white_24dp"
tools:tint="@color/purple_400" />
</FrameLayout>
</LinearLayout>
<ImageView
android:id="@+id/quote_dismiss"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="4dp"
android:layout_gravity="top|end"
android:background="@drawable/dismiss_background"
android:src="@drawable/ic_close_white_18dp"
android:tint="@color/gray70" />
</FrameLayout>
</merge>

@ -243,7 +243,11 @@
</declare-styleable>
<declare-styleable name="QuoteView">
<attr name="quote_dismissable" format="boolean"/>
<attr name="message_type" format="enum">
<enum name="preview" value="0" />
<enum name="outgoing" value="1" />
<enum name="incoming" value="2" />
</attr>
</declare-styleable>

@ -34,6 +34,7 @@
<color name="transparent_white_20">#20ffffff</color>
<color name="transparent_white_30">#30ffffff</color>
<color name="transparent_white_40">#40ffffff</color>
<color name="transparent_white_70">#70ffffff</color>
<color name="transparent_white_aa">#aaffffff</color>
<color name="conversation_compose_divider">#32000000</color>

@ -26,6 +26,9 @@
<dimen name="media_bubble_min_height">100dp</dimen>
<dimen name="media_bubble_max_height">320dp</dimen>
<dimen name="quote_corner_radius">3dp</dimen>
<dimen name="quote_outline_width">1dp</dimen>
<integer name="media_overview_cols">3</integer>
<dimen name="message_details_table_row_pad">10dp</dimen>

@ -194,6 +194,7 @@
<string name="ConversationFragment_sms">SMS</string>
<string name="ConversationFragment_deleting">Deleting</string>
<string name="ConversationFragment_deleting_messages">Deleting messages...</string>
<string name="ConversationFragment_quoted_message_not_found">Quoted message not found</string>
<!-- ConversationListActivity -->
<string name="ConversationListActivity_there_is_no_browser_installed_on_your_device">There is no browser installed on your device.</string>
@ -813,6 +814,13 @@
<string name="audio_view__pause_accessibility_description">Pause</string>
<string name="audio_view__download_accessibility_description">Download</string>
<!-- QuoteView -->
<string name="QuoteView_audio">Audio</string>
<string name="QuoteView_video">Video</string>
<string name="QuoteView_photo">Photo</string>
<string name="QuoteView_document">Document</string>
<string name="QuoteView_you">You</string>
<!-- conversation_fragment_cab -->
<string name="conversation_fragment_cab__batch_selection_mode">Batch selection mode</string>
<string name="conversation_fragment_cab__batch_selection_amount">%s selected</string>

@ -1,9 +1,11 @@
package org.thoughtcrime.securesms;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -11,11 +13,18 @@ import java.util.Locale;
import java.util.Set;
public interface BindableConversationItem extends Unbindable {
void bind(@NonNull MessageRecord messageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
void bind(@NonNull MessageRecord messageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient recipients);
@NonNull Recipient recipients,
boolean pulseHighlight);
MessageRecord getMessageRecord();
void setEventListener(@Nullable EventListener listener);
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
}
}

@ -65,7 +65,6 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.annimon.stream.Stream;
import com.google.android.gms.location.places.ui.PlacePicker;
import com.google.protobuf.ByteString;
@ -2063,7 +2062,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
inputPanel.setQuote(GlideApp.with(this),
messageRecord.getTimestamp(),
messageRecord.getDateSent(),
author,
messageRecord.getBody(),
messageRecord.isMms() ? ((MmsMessageRecord)messageRecord).getSlideDeck() : new SlideDeck());

@ -33,7 +33,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
@ -101,6 +100,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private final @NonNull Calendar calendar;
private final @NonNull MessageDigest digest;
private MessageRecord recordToPulseHighlight;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
super(itemView);
@ -132,7 +133,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
}
interface ItemClickListener {
interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(MessageRecord item);
void onItemLongClick(MessageRecord item);
}
@ -190,7 +191,10 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override
protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) {
long start = System.currentTimeMillis();
viewHolder.getView().bind(messageRecord, glideRequests, locale, batchSelected, recipient);
viewHolder.getView().bind(messageRecord, glideRequests, locale, batchSelected, recipient, messageRecord == recordToPulseHighlight);
if (messageRecord == recordToPulseHighlight) {
recordToPulseHighlight = null;
}
Log.w(TAG, "Bind time: " + (System.currentTimeMillis() - start));
}
@ -209,6 +213,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
}
return true;
});
itemView.setEventListener(clickListener);
Log.w(TAG, "Inflate time: " + (System.currentTimeMillis() - start));
return new ViewHolder(itemView);
}
@ -341,6 +346,13 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
return Collections.unmodifiableSet(new HashSet<>(batchSelected));
}
public void pulseHighlightItem(int position) {
if (position < getItemCount()) {
recordToPulseHighlight = getRecordForPositionOrThrow(position);
notifyItemChanged(position);
}
}
private boolean hasAudio(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
}

@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.Slide;
@ -226,6 +227,7 @@ public class ConversationFragment extends Fragment
if (messageRecords.size() > 1) {
menu.findItem(R.id.menu_context_forward).setVisible(false);
menu.findItem(R.id.menu_context_reply).setVisible(false);
menu.findItem(R.id.menu_context_details).setVisible(false);
menu.findItem(R.id.menu_context_save_attachment).setVisible(false);
menu.findItem(R.id.menu_context_resend).setVisible(false);
@ -240,6 +242,9 @@ public class ConversationFragment extends Fragment
menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_reply).setVisible(!messageRecord.isPending() &&
!messageRecord.isFailed() &&
messageRecord.isSecure());
}
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage && hasText);
}
@ -414,7 +419,6 @@ public class ConversationFragment extends Fragment
return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT), lastSeen);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
Log.w(TAG, "onLoadFinished");
@ -593,7 +597,6 @@ public class ConversationFragment extends Fragment
setCorrectMenuVisibility(actionMode.getMenu());
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size()));
}
}
}
@ -606,6 +609,37 @@ public class ConversationFragment extends Fragment
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
}
}
@Override
public void onQuoteClicked(MmsMessageRecord messageRecord) {
if (messageRecord.getQuote() == null) {
Log.w(TAG, "Received a 'quote clicked' event, but there's no quote...");
return;
}
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... voids) {
return DatabaseFactory.getMmsSmsDatabase(getContext())
.getQuotedMessagePosition(threadId, messageRecord.getQuote().getId(), messageRecord.getQuote().getAuthor());
}
@Override
protected void onPostExecute(Integer position) {
if (position >= 0 && position < getListAdapter().getItemCount()) {
list.scrollToPosition(position);
getListAdapter().pulseHighlightItem(position);
} else {
Toast.makeText(getContext(), getResources().getText(R.string.ConversationFragment_quoted_message_not_found), Toast.LENGTH_SHORT).show();
if (position < 0) {
Log.w(TAG, "Tried to navigate to quoted message, but it was deleted.");
} else {
Log.w(TAG, "Tried to navigate to quoted message, but it was out of the bounds of the adapter.");
}
}
}
}.execute();
}
}
private class ActionModeCallback implements ActionMode.Callback {

@ -22,6 +22,7 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.net.Uri;
@ -126,17 +127,18 @@ public class ConversationItem extends LinearLayout
private DeliveryStatusView deliveryStatusIndicator;
private AlertView alertView;
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
private @NonNull Recipient conversationRecipient;
private @NonNull Stub<ThumbnailView> mediaThumbnailStub;
private @NonNull Stub<AudioView> audioViewStub;
private @NonNull Stub<DocumentView> documentViewStub;
private @NonNull ExpirationTimerView expirationTimer;
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
private @NonNull Recipient conversationRecipient;
private @NonNull Stub<ThumbnailView> mediaThumbnailStub;
private @NonNull Stub<AudioView> audioViewStub;
private @NonNull Stub<DocumentView> documentViewStub;
private @NonNull ExpirationTimerView expirationTimer;
private @Nullable EventListener eventListener;
private int defaultBubbleColor;
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final Context context;
@ -191,7 +193,8 @@ public class ConversationItem extends LinearLayout
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient)
@NonNull Recipient conversationRecipient,
boolean pulseHighlight)
{
this.messageRecord = messageRecord;
this.locale = locale;
@ -205,7 +208,7 @@ public class ConversationItem extends LinearLayout
this.conversationRecipient.addListener(this);
setMediaAttributes(messageRecord);
setInteractionState(messageRecord);
setInteractionState(messageRecord, pulseHighlight);
setBodyText(messageRecord);
setBubbleState(messageRecord, recipient);
setStatusIcons(messageRecord);
@ -217,6 +220,11 @@ public class ConversationItem extends LinearLayout
setQuote(messageRecord);
}
@Override
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
@ -243,6 +251,27 @@ public class ConversationItem extends LinearLayout
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (hasQuote(messageRecord)) {
int quoteWidth = quoteView.getMeasuredWidth();
int availableWidth;
if (hasThumbnail(messageRecord)) {
availableWidth = mediaThumbnailStub.get().getMeasuredWidth();
} else {
availableWidth = bodyBubble.getMeasuredWidth() - bodyBubble.getPaddingLeft() - bodyBubble.getPaddingRight();
}
if (quoteWidth != availableWidth) {
quoteView.getLayoutParams().width = availableWidth;
measure(widthMeasureSpec, heightMeasureSpec);
}
}
}
private void initializeAttributes() {
final int[] attributes = new int[] {R.attr.conversation_item_bubble_background};
final TypedArray attrs = context.obtainStyledAttributes(attributes);
@ -309,8 +338,17 @@ public class ConversationItem extends LinearLayout
}
}
private void setInteractionState(MessageRecord messageRecord) {
setSelected(batchSelected.contains(messageRecord));
private void setInteractionState(MessageRecord messageRecord, boolean pulseHighlight) {
if (batchSelected.contains(messageRecord)) {
setBackgroundResource(R.drawable.conversation_item_background);
setSelected(true);
} else if (pulseHighlight) {
setBackgroundResource(R.drawable.conversation_item_background_animated);
setSelected(true);
postDelayed(() -> setSelected(false), 500);
} else {
setSelected(false);
}
if (mediaThumbnailStub.resolved()) {
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
@ -346,6 +384,10 @@ public class ConversationItem extends LinearLayout
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
}
private boolean hasQuote(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getQuote() != null;
}
private void setBodyText(MessageRecord messageRecord) {
bodyText.setClickable(false);
bodyText.setFocusable(false);
@ -517,6 +559,16 @@ public class ConversationItem extends LinearLayout
assert quote != null;
quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quote.getText(), quote.getAttachment());
quoteView.setVisibility(View.VISIBLE);
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
quoteView.setOnClickListener(view -> {
if (eventListener != null && batchSelected.isEmpty()) {
eventListener.onQuoteClicked((MmsMessageRecord) messageRecord);
} else {
passthroughClickListener.onClick(view);
}
});
quoteView.setOnLongClickListener(passthroughClickListener);
} else {
quoteView.dismiss();
}

@ -66,17 +66,23 @@ public class ConversationUpdateItem extends LinearLayout
}
@Override
public void bind(@NonNull MessageRecord messageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
public void bind(@NonNull MessageRecord messageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient)
@NonNull Recipient conversationRecipient,
boolean pulseUpdate)
{
this.batchSelected = batchSelected;
bind(messageRecord, locale);
}
@Override
public void setEventListener(@Nullable EventListener listener) {
// No events to report yet
}
@Override
public MessageRecord getMessageRecord() {
return messageRecord;

@ -252,7 +252,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
conversationItem.bind(messageRecord, glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient);
conversationItem.bind(messageRecord, glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, false);
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
}

@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import java.util.LinkedList;
import java.util.List;
@ -16,7 +17,7 @@ public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType, int transferState, long size,
@Nullable String fileName, @NonNull String location,
@Nullable String key, @NonNull String relay,
@Nullable String key, @Nullable String relay,
@Nullable byte[] digest, boolean voiceNote,
int width, int height)
{
@ -52,6 +53,22 @@ public class PointerAttachment extends Attachment {
return results;
}
public static List<Attachment> forPointers(List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
List<Attachment> results = new LinkedList<>();
if (pointers != null) {
for (SignalServiceDataMessage.Quote.QuotedAttachment pointer : pointers) {
Optional<Attachment> result = forPointer(pointer);
if (result.isPresent()) {
results.add(result.get());
}
}
}
return results;
}
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer) {
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.absent();
@ -73,4 +90,20 @@ public class PointerAttachment extends Attachment {
pointer.get().asPointer().getHeight()));
}
public static Optional<Attachment> forPointer(SignalServiceDataMessage.Quote.QuotedAttachment pointer) {
SignalServiceAttachment thumbnail = pointer.getThumbnail();
return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
pointer.getFileName(),
String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0),
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0));
}
}

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.color;
import android.content.Context;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.util.TypedValue;
@ -61,27 +62,57 @@ public enum MaterialColor {
}
public int toConversationColor(@NonNull Context context) {
if (getAttribute(context, R.attr.theme_type, "light").equals("dark")) {
return context.getResources().getColor(conversationColorDark);
} else {
return context.getResources().getColor(conversationColorLight);
}
return context.getResources().getColor(isDarkTheme(context) ? conversationColorDark
: conversationColorLight);
}
public int toActionBarColor(@NonNull Context context) {
if (getAttribute(context, R.attr.theme_type, "light").equals("dark")) {
return context.getResources().getColor(actionBarColorDark);
} else {
return context.getResources().getColor(actionBarColorLight);
}
return context.getResources().getColor(isDarkTheme(context) ? actionBarColorDark
: actionBarColorLight);
}
public int toStatusBarColor(@NonNull Context context) {
if (getAttribute(context, R.attr.theme_type, "light").equals("dark")) {
return context.getResources().getColor(statusBarColorDark);
} else {
return context.getResources().getColor(statusBarColorLight);
return context.getResources().getColor(isDarkTheme(context) ? statusBarColorDark
: statusBarColorLight);
}
public int toQuoteTitleColor(@NonNull Context context) {
return context.getResources().getColor(conversationColorDark);
}
public int toQuoteBarColorResource(@NonNull Context context, boolean outgoing) {
if (outgoing) {
return conversationColorDark;
}
return R.color.white;
}
public int toQuoteBackgroundColor(@NonNull Context context, boolean outgoing) {
if (outgoing) {
int color = toConversationColor(context);
return Color.argb(0x44, Color.red(color), Color.green(color), Color.blue(color));
}
return context.getResources().getColor(isDarkTheme(context) ? R.color.transparent_white_70
: R.color.transparent_white_aa);
}
public int toQuoteOutlineColor(@NonNull Context context, boolean outgoing) {
if (!outgoing) {
return context.getResources().getColor(R.color.transparent_white_70);
}
return context.getResources().getColor(isDarkTheme(context) ? R.color.transparent_white_40
: R.color.grey_400_transparent);
}
public int toQuoteIconForegroundColor(@NonNull Context context, boolean outgoing) {
if (outgoing) {
return context.getResources().getColor(R.color.white);
}
return toConversationColor(context);
}
public int toQuoteIconBackgroundColor(@NonNull Context context, boolean outgoing) {
return context.getResources().getColor(toQuoteBarColorResource(context, outgoing));
}
public boolean represents(Context context, int colorValue) {
@ -107,6 +138,9 @@ public enum MaterialColor {
}
}
private boolean isDarkTheme(@NonNull Context context) {
return getAttribute(context, R.attr.theme_type, "light").equals("dark");
}
public static MaterialColor fromSerialized(String serialized) throws UnknownColorException {
for (MaterialColor color : MaterialColor.values()) {

@ -3,19 +3,22 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.annimon.stream.Stream;
@ -23,11 +26,14 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.DocumentSlide;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.Util;
@ -38,19 +44,31 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
private static final String TAG = QuoteView.class.getSimpleName();
private static final int MESSAGE_TYPE_PREVIEW = 0;
private static final int MESSAGE_TYPE_OUTGOING = 1;
private static final int MESSAGE_TYPE_INCOMING = 2;
private View rootView;
private TextView authorView;
private TextView bodyView;
private ImageView quoteBarView;
private ImageView attachmentView;
private ImageView attachmentVideoOverlayView;
private ViewGroup attachmentIconContainerView;
private ImageView attachmentIconView;
private ImageView attachmentIconBackgroundView;
private ImageView dismissView;
private long id;
private Recipient author;
private String body;
private View mediaDescription;
private ImageView mediaDescriptionIcon;
private TextView mediaDescriptionText;
private SlideDeck attachments;
private int messageType;
private int roundedCornerRadiusPx;
private final Path clipPath = new Path();
private final RectF drawRect = new RectF();
public QuoteView(Context context) {
super(context);
@ -76,27 +94,47 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
private void initialize(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.quote_view, this);
this.authorView = findViewById(R.id.quote_author);
this.bodyView = findViewById(R.id.quote_text);
this.quoteBarView = findViewById(R.id.quote_bar);
this.attachmentView = findViewById(R.id.quote_attachment);
this.dismissView = findViewById(R.id.quote_dismiss);
this.mediaDescriptionIcon = findViewById(R.id.media_icon);
this.mediaDescriptionText = findViewById(R.id.media_name);
this.mediaDescription = findViewById(R.id.media_description);
this.rootView = findViewById(R.id.quote_root);
this.authorView = findViewById(R.id.quote_author);
this.bodyView = findViewById(R.id.quote_text);
this.quoteBarView = findViewById(R.id.quote_bar);
this.attachmentView = findViewById(R.id.quote_attachment);
this.attachmentVideoOverlayView = findViewById(R.id.quote_video_overlay);
this.attachmentIconContainerView = findViewById(R.id.quote_attachment_icon_container);
this.attachmentIconView = findViewById(R.id.quote_attachment_icon);
this.attachmentIconBackgroundView = findViewById(R.id.quote_attachment_icon_background);
this.dismissView = findViewById(R.id.quote_dismiss);
this.mediaDescriptionText = findViewById(R.id.media_name);
this.roundedCornerRadiusPx = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
boolean dismissable = typedArray.getBoolean(R.styleable.QuoteView_quote_dismissable, true);
messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0);
typedArray.recycle();
if (!dismissable) dismissView.setVisibility(View.GONE);
else dismissView.setVisibility(View.VISIBLE);
dismissView.setVisibility(messageType == MESSAGE_TYPE_PREVIEW ? VISIBLE : GONE);
}
dismissView.setOnClickListener(view -> setVisibility(View.GONE));
dismissView.setOnClickListener(view -> setVisibility(GONE));
setBackgroundDrawable(getContext().getResources().getDrawable(R.drawable.quote_background));
setWillNotDraw(false);
if (Build.VERSION.SDK_INT < 18) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawRect.left = 0;
drawRect.top = 0;
drawRect.right = getWidth();
drawRect.bottom = getHeight();
clipPath.reset();
clipPath.addRoundRect(drawRect, roundedCornerRadiusPx, roundedCornerRadiusPx, Path.Direction.CW);
canvas.clipPath(clipPath);
}
public void setQuote(GlideRequests glideRequests, long id, @NonNull Recipient author, @Nullable String body, @NonNull SlideDeck attachments) {
@ -110,7 +148,7 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
author.addListener(this);
setQuoteAuthor(author);
setQuoteText(body, attachments);
setQuoteAttachment(glideRequests, attachments);
setQuoteAttachment(glideRequests, attachments, author);
}
public void dismiss() {
@ -120,7 +158,7 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
this.author = null;
this.body = null;
setVisibility(View.GONE);
setVisibility(GONE);
}
@Override
@ -133,54 +171,96 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
}
private void setQuoteAuthor(@NonNull Recipient author) {
this.authorView.setText(author.toShortString());
this.authorView.setTextColor(author.getColor().toActionBarColor(getContext()));
this.quoteBarView.setColorFilter(author.getColor().toActionBarColor(getContext()), PorterDuff.Mode.SRC_IN);
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress());
authorView.setText(isOwnNumber ? getContext().getString(R.string.QuoteView_you)
: author.toShortString());
authorView.setTextColor(author.getColor().toQuoteTitleColor(getContext()));
// We use the raw color resource because Android 4.x was struggling with tints here
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
GradientDrawable background = (GradientDrawable) rootView.getBackground();
background.setColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
background.setStroke(getResources().getDimensionPixelSize(R.dimen.quote_outline_width),
author.getColor().toQuoteOutlineColor(getContext(), outgoing));
}
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
if (TextUtils.isEmpty(body) && attachments.containsMediaSlide()) {
mediaDescription.setVisibility(View.VISIBLE);
bodyView.setVisibility(View.GONE);
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
bodyView.setVisibility(VISIBLE);
bodyView.setText(body == null ? "" : body);
mediaDescriptionText.setVisibility(GONE);
return;
}
if (!audioSlides.isEmpty()) {
mediaDescriptionIcon.setImageResource(R.drawable.ic_mic_white_24dp);
mediaDescriptionText.setText("Audio");
} else if (!documentSlides.isEmpty()) {
mediaDescriptionIcon.setImageResource(R.drawable.ic_insert_drive_file_white_24dp);
mediaDescriptionText.setText(String.format("%s (%s)", documentSlides.get(0).getFileName(), Util.getPrettyFileSize(documentSlides.get(0).getFileSize())));
} else if (!videoSlides.isEmpty()) {
mediaDescriptionIcon.setImageResource(R.drawable.ic_videocam_white_24dp);
mediaDescriptionText.setText("Video");
} else if (!imageSlides.isEmpty()) {
mediaDescriptionIcon.setImageResource(R.drawable.ic_camera_alt_white_24dp);
mediaDescriptionText.setText("Photo");
bodyView.setVisibility(GONE);
mediaDescriptionText.setVisibility(VISIBLE);
mediaDescriptionText.setTypeface(null, Typeface.ITALIC);
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
// Given that most types have images, we specifically check images last
if (!audioSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_audio);
} else if (!documentSlides.isEmpty()) {
String filename = documentSlides.get(0).getFileName().orNull();
if (!TextUtils.isEmpty(filename)) {
mediaDescriptionText.setTypeface(null, Typeface.NORMAL);
mediaDescriptionText.setText(filename);
} else {
mediaDescriptionText.setText(R.string.QuoteView_document);
}
} else {
mediaDescription.setVisibility(View.GONE);
bodyView.setVisibility(View.VISIBLE);
bodyView.setText(body == null ? "" : body);
} else if (!videoSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_video);
} else if (!imageSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_photo);
}
}
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
private void setQuoteAttachment(@NonNull GlideRequests glideRequests,
@NonNull SlideDeck slideDeck,
@NonNull Recipient author)
{
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo()).limit(1).toList();
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
attachmentVideoOverlayView.setVisibility(GONE);
if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
attachmentView.setVisibility(View.VISIBLE);
dismissView.setBackgroundResource(R.drawable.circle_alpha);
attachmentView.setVisibility(VISIBLE);
attachmentIconContainerView.setVisibility(GONE);
dismissView.setBackgroundResource(R.drawable.dismiss_background);
if (imageVideoSlides.get(0).hasVideo()) {
attachmentVideoOverlayView.setVisibility(VISIBLE);
}
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(attachmentView);
} else if (!audioSlides.isEmpty() || !documentSlides.isEmpty()){
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
dismissView.setBackgroundResource(R.drawable.circle_alpha);
attachmentView.setVisibility(GONE);
attachmentIconContainerView.setVisibility(VISIBLE);
if (!audioSlides.isEmpty()) {
attachmentIconView.setImageResource(R.drawable.ic_mic_white_48dp);
} else {
attachmentIconView.setImageResource(R.drawable.ic_insert_drive_file_white_24dp);
}
attachmentIconView.setColorFilter(author.getColor().toQuoteIconForegroundColor(getContext(), outgoing), PorterDuff.Mode.SRC_IN);
attachmentIconBackgroundView.setColorFilter(author.getColor().toQuoteIconBackgroundColor(getContext(), outgoing), PorterDuff.Mode.SRC_IN);
} else {
attachmentView.setVisibility(View.GONE);
attachmentView.setVisibility(GONE);
attachmentIconContainerView.setVisibility(GONE);
dismissView.setBackgroundDrawable(null);
}
}

@ -46,6 +46,8 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
@ -313,13 +315,20 @@ public class AttachmentDatabase extends Database {
public void insertAttachmentsForPlaceholder(long mmsId, @NonNull AttachmentId attachmentId, @NonNull InputStream inputStream)
throws MmsException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
DataInfo dataInfo = setAttachmentData(inputStream);
ContentValues values = new ContentValues();
DatabaseAttachment placeholder = getAttachment(attachmentId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
DataInfo dataInfo = setAttachmentData(inputStream);
if (placeholder != null && placeholder.isQuote() && !placeholder.getContentType().startsWith("image")) {
values.put(THUMBNAIL, dataInfo.file.getAbsolutePath());
values.put(THUMBNAIL_RANDOM, dataInfo.random);
} else {
values.put(DATA, dataInfo.file.getAbsolutePath());
values.put(SIZE, dataInfo.length);
values.put(DATA_RANDOM, dataInfo.random);
}
values.put(DATA, dataInfo.file.getAbsolutePath());
values.put(SIZE, dataInfo.length);
values.put(DATA_RANDOM, dataInfo.random);
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
values.put(CONTENT_LOCATION, (String)null);
values.put(CONTENT_DISPOSITION, (String)null);
@ -635,8 +644,22 @@ public class AttachmentDatabase extends Database {
long rowId = database.insert(TABLE_NAME, null, contentValues);
AttachmentId attachmentId = new AttachmentId(rowId, uniqueId);
Uri thumbnailUri = attachment.getThumbnailUri();
boolean hasThumbnail = false;
if (thumbnailUri != null) {
try (InputStream attachmentStream = PartAuthority.getAttachmentStream(context, thumbnailUri)) {
Pair<Integer, Integer> dimens = BitmapUtil.getDimensions(attachmentStream);
updateAttachmentThumbnail(attachmentId,
PartAuthority.getAttachmentStream(context, thumbnailUri),
(float) dimens.first / (float) dimens.second);
hasThumbnail = true;
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, "Failed to save existing thumbnail.", e);
}
}
if (dataInfo != null) {
if (!hasThumbnail && dataInfo != null) {
if (MediaUtil.hasVideoThumbnail(attachment.getDataUri())) {
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri());

@ -27,6 +27,7 @@ import net.sqlcipher.database.SQLiteQueryBuilder;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.util.Util;
import java.util.HashSet;
import java.util.Set;
@ -140,6 +141,26 @@ public class MmsSmsDatabase extends Database {
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, false, true);
}
public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull Address address) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
String serializedAddress = address.serialize();
boolean isOwnNumber = Util.isOwnNumber(context, address);
while (cursor != null && cursor.moveToNext()) {
boolean quoteIdMatches = cursor.getLong(0) == quoteId;
boolean addressMatches = serializedAddress.equals(cursor.getString(1));
if (quoteIdMatches && (addressMatches || isOwnNumber)) {
return cursor.getPosition();
}
}
}
return -1;
}
private Cursor queryTables(String[] projection, String selection, String order, String limit) {
String[] mmsProjection = {MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,

@ -43,8 +43,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int MIGRATE_SESSIONS_VERSION = 4;
private static final int NO_MORE_IMAGE_THUMBNAILS_VERSION = 5;
private static final int ATTACHMENT_DIMENSIONS = 6;
private static final int QUOTED_REPLIES = 7;
private static final int DATABASE_VERSION = 6;
private static final int DATABASE_VERSION = 7;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -167,6 +168,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN height INTEGER DEFAULT 0");
}
if (oldVersion < QUOTED_REPLIES) {
db.execSQL("ALTER TABLE mms ADD COLUMN quote_id INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE mms ADD COLUMN quote_author TEXT");
db.execSQL("ALTER TABLE mms ADD COLUMN quote_body TEXT");
db.execSQL("ALTER TABLE mms ADD COLUMN quote_attachment INTEGER DEFAULT -1");
db.execSQL("ALTER TABLE part ADD COLUMN quote INTEGER DEFAULT 0");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

@ -38,11 +38,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -894,7 +894,7 @@ public class PushDecryptJob extends ContextJob {
return Optional.of(new QuoteModel(quote.get().getId(),
author,
quote.get().getText(),
PointerAttachment.forPointers(Optional.of(quote.get().getAttachments()))));
PointerAttachment.forPointers(quote.get().getAttachments())));
}
private Optional<InsertResult> insertPlaceholder(@NonNull SignalServiceEnvelope envelope) {

@ -28,6 +28,7 @@ import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -120,34 +121,35 @@ public abstract class PushSendJob extends SendJob {
protected Optional<SignalServiceDataMessage.Quote> getQuoteFor(OutgoingMediaMessage message) {
if (message.getOutgoingQuote() == null) return Optional.absent();
long quoteId = message.getOutgoingQuote().getId();
String quoteBody = message.getOutgoingQuote().getText();
Address quoteAuthor = message.getOutgoingQuote().getAuthor();
List<SignalServiceAttachment> quoteAttachments = new LinkedList<>();
long quoteId = message.getOutgoingQuote().getId();
String quoteBody = message.getOutgoingQuote().getText();
Address quoteAuthor = message.getOutgoingQuote().getAuthor();
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
for (Attachment attachment : message.getOutgoingQuote().getAttachments()) {
BitmapUtil.ScaleResult attachmentData = null;
BitmapUtil.ScaleResult thumbnailData = null;
SignalServiceAttachment thumbnail = null;
try {
if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) {
attachmentData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024);
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024);
} else if (MediaUtil.isVideoType(attachment.getContentType()) && attachment.getThumbnailUri() != null) {
attachmentData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
}
if (attachmentData != null) {
quoteAttachments.add(SignalServiceAttachment.newStreamBuilder()
.withContentType("image/jpeg")
.withFileName(attachment.getFileName())
.withHeight(attachmentData.getHeight())
.withWidth(attachmentData.getWidth())
.withLength(attachmentData.getBitmap().length)
.withStream(new ByteArrayInputStream(attachmentData.getBitmap()))
.build());
} else {
quoteAttachments.add(new SignalServiceAttachmentPointer(0, attachment.getContentType(), null, null, Optional.absent(), Optional.absent(), 0, 0, Optional.absent(), Optional.fromNullable(attachment.getFileName()), attachment.isVoiceNote()));
if (thumbnailData != null) {
thumbnail = SignalServiceAttachment.newStreamBuilder()
.withContentType("image/jpeg")
.withWidth(thumbnailData.getWidth())
.withHeight(thumbnailData.getHeight())
.withLength(thumbnailData.getBitmap().length)
.withStream(new ByteArrayInputStream(thumbnailData.getBitmap()))
.build();
}
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
attachment.getFileName(),
thumbnail));
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
}

Loading…
Cancel
Save