mirror of
https://github.com/deltachat/deltachat-android.git
synced 2025-10-04 02:09:39 +02:00
Add swipe-to-reply quotes
This commit is contained in:
parent
924695d6cf
commit
c720df5d5c
43 changed files with 1667 additions and 176 deletions
|
@ -1410,6 +1410,27 @@ JNIEXPORT void Java_com_b44t_messenger_DcMsg_setLocation(JNIEnv *env, jobject ob
|
|||
}
|
||||
|
||||
|
||||
JNIEXPORT void Java_com_b44t_messenger_DcMsg_setQuoteCPtr(JNIEnv *env, jobject obj, jlong quoteCPtr)
|
||||
{
|
||||
dc_msg_set_quote(get_dc_msg(env, obj), (dc_msg_t*)quoteCPtr);
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getQuotedText(JNIEnv *env, jobject obj)
|
||||
{
|
||||
char* temp = dc_msg_get_quoted_text(get_dc_msg(env, obj));
|
||||
jstring ret = JSTRING_NEW(temp);
|
||||
dc_str_unref(temp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong Java_com_b44t_messenger_DcMsg_getQuotedMsgCPtr(JNIEnv *env, jobject obj)
|
||||
{
|
||||
return (jlong)dc_msg_get_quoted_msg(get_dc_msg(env, obj));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* DcContact
|
||||
|
|
BIN
res/drawable-hdpi/ic_close_white_18dp.webp
Normal file
BIN
res/drawable-hdpi/ic_close_white_18dp.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 182 B |
BIN
res/drawable-mdpi/ic_close_white_18dp.webp
Normal file
BIN
res/drawable-mdpi/ic_close_white_18dp.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 B |
BIN
res/drawable-xhdpi/ic_close_white_18dp.webp
Normal file
BIN
res/drawable-xhdpi/ic_close_white_18dp.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 198 B |
BIN
res/drawable-xxhdpi/ic_close_white_18dp.webp
Normal file
BIN
res/drawable-xxhdpi/ic_close_white_18dp.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 226 B |
BIN
res/drawable-xxxhdpi/ic_close_white_18dp.webp
Normal file
BIN
res/drawable-xxxhdpi/ic_close_white_18dp.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 284 B |
5
res/drawable/circle_alpha.xml
Normal file
5
res/drawable/circle_alpha.xml
Normal file
|
@ -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="#22000000" />
|
||||
</shape>
|
5
res/drawable/dismiss_background.xml
Normal file
5
res/drawable/dismiss_background.xml
Normal file
|
@ -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>
|
|
@ -7,7 +7,6 @@
|
|||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:clickable="true"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
|
@ -16,25 +15,17 @@
|
|||
android:background="?attr/input_panel_bg_color">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/input_field_frame_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@id/quote_view"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/recording_container"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -183,11 +174,10 @@
|
|||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/input_field_frame_layout"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:id="@+id/button_toggle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
|
@ -221,4 +211,15 @@
|
|||
android:background="@drawable/circle_touch_highlight_background" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
|
||||
<org.thoughtcrime.securesms.components.QuoteView
|
||||
android:id="@+id/quote_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:visibility="gone"
|
||||
app:message_type="preview"
|
||||
tools:visibility="visible" />
|
||||
</org.thoughtcrime.securesms.components.InputPanel>
|
||||
|
|
|
@ -25,6 +25,18 @@
|
|||
android:clipToPadding="false"
|
||||
android:clipChildren="false">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/reply_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_alignStart="@id/body_bubble"
|
||||
android:layout_alignTop="@id/body_bubble"
|
||||
android:layout_alignBottom="@id/body_bubble"
|
||||
android:alpha="0"
|
||||
app:srcCompat="?menu_reply_icon"
|
||||
android:tint="?icon_tint"
|
||||
android:layout_alignLeft="@id/body_bubble" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/contact_photo_container"
|
||||
android:layout_width="36dp"
|
||||
|
@ -87,6 +99,19 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.QuoteView
|
||||
android:id="@+id/quote_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:visibility="gone"
|
||||
app:message_type="incoming"
|
||||
app:quote_colorPrimary="?attr/conversation_item_quote_text_color"
|
||||
app:quote_colorSecondary="?attr/conversation_item_quote_text_color"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/image_view_stub"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -23,6 +23,18 @@
|
|||
android:clipToPadding="false"
|
||||
android:clipChildren="false">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/reply_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_alignStart="@id/body_bubble"
|
||||
android:layout_alignTop="@id/body_bubble"
|
||||
android:layout_alignBottom="@id/body_bubble"
|
||||
android:alpha="0"
|
||||
app:srcCompat="?menu_reply_icon"
|
||||
android:tint="?icon_tint"
|
||||
android:layout_alignLeft="@id/body_bubble" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/body_bubble"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -65,6 +77,19 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.QuoteView
|
||||
android:id="@+id/quote_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:visibility="gone"
|
||||
app:message_type="incoming"
|
||||
app:quote_colorPrimary="?attr/conversation_item_quote_text_color"
|
||||
app:quote_colorSecondary="?attr/conversation_item_quote_text_color"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/image_view_stub"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
150
res/layout/quote_view.xml
Normal file
150
res/layout/quote_view.xml
Normal file
|
@ -0,0 +1,150 @@
|
|||
<?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:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="6dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/quote_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quote_bar"
|
||||
android:layout_width="@dimen/quote_corner_radius_bottom"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/purple_400"
|
||||
tools:tint="@color/purple_400" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="1">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/quote_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Signal.Text.Caption"
|
||||
android:textColor="@color/core_black"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:visibility="gone"
|
||||
tools:text="Peter Parker" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/media_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
style="@style/Signal.Text.Caption"
|
||||
android:textColor="@color/gray95"
|
||||
android:textStyle="italic"
|
||||
android:visibility="gone"
|
||||
tools:text="Photo"
|
||||
tools:visibility="visible"
|
||||
android:layout_marginRight="8dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/quote_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Signal.Text.Quote"
|
||||
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="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/quote_attachment_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.CircleColorImageView
|
||||
android:id="@+id/quote_attachment_icon"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:contentDescription="@string/file"
|
||||
android:padding="4dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_insert_drive_file_white_24dp"
|
||||
app:circleColor="@color/document_icon" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quote_thumbnail"
|
||||
android:layout_width="@dimen/quote_thumb_size"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/quote_video_overlay"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/circle_universal_overlay"
|
||||
android:layout_gravity="center"
|
||||
android:longClickable="false"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="13dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="11dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/triangle_right"
|
||||
android:layout_marginLeft="11dp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quote_dismiss"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:background="@drawable/dismiss_background"
|
||||
android:src="@drawable/ic_close_white_18dp"
|
||||
android:tint="?quote_dismiss_button_tint"
|
||||
android:layout_marginRight="6dp" />
|
||||
|
||||
</merge>
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
<item android:title="@string/menu_reply"
|
||||
android:id="@+id/menu_context_reply"
|
||||
android:visible="false"
|
||||
android:icon="?menu_reply_icon"
|
||||
app:showAsAction="always" />
|
||||
|
||||
|
@ -37,4 +36,7 @@
|
|||
android:icon="?menu_forward_icon"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item android:title="@string/reply_privately"
|
||||
android:id="@+id/menu_context_reply_privately"
|
||||
app:showAsAction="collapseActionView" />
|
||||
</menu>
|
||||
|
|
|
@ -119,6 +119,9 @@
|
|||
|
||||
<attr name="pref_icon_tint" format="color"/>
|
||||
|
||||
<attr name="quote_missing_icon_color" format="color" />
|
||||
<attr name="quote_dismiss_button_tint" format="color" />
|
||||
|
||||
<declare-styleable name="CustomDefaultPreference">
|
||||
<attr name="custom_pref_toggle" format="string"/>
|
||||
</declare-styleable>
|
||||
|
@ -236,4 +239,18 @@
|
|||
<attr name="timeFrame" format="integer"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="QuoteView">
|
||||
<attr name="message_type" format="enum">
|
||||
<enum name="preview" value="0" />
|
||||
<enum name="outgoing" value="1" />
|
||||
<enum name="incoming" value="2" />
|
||||
</attr>
|
||||
<attr name="quote_colorPrimary" format="color" />
|
||||
<attr name="quote_colorSecondary" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="AdaptiveActionsToolbar">
|
||||
<attr name="aat_max_shown" format="integer" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<dimen name="message_bubble_shadow_distance">1.5dp</dimen>
|
||||
<dimen name="message_bubble_horizontal_padding">8dp</dimen>
|
||||
<dimen name="message_bubble_horizontal_padding_half">4dp</dimen>
|
||||
<dimen name="message_bubble_top_padding">8dp</dimen>
|
||||
<dimen name="message_bubble_top_padding">6dp</dimen>
|
||||
<dimen name="message_bubble_collapsed_footer_padding">6dp</dimen>
|
||||
<dimen name="message_bubble_edge_margin">32dp</dimen>
|
||||
<dimen name="message_bubble_bottom_padding">6dp</dimen>
|
||||
|
@ -41,6 +41,10 @@
|
|||
<dimen name="conversation_group_left_gutter">52dp</dimen>
|
||||
<dimen name="conversation_vertical_message_spacing_collapse">1dp</dimen>
|
||||
|
||||
<dimen name="quote_corner_radius_large">10dp</dimen>
|
||||
<dimen name="quote_corner_radius_bottom">2.5dp</dimen>
|
||||
<dimen name="quote_corner_radius_preview">18dp</dimen>
|
||||
|
||||
<dimen name="quick_camera_shutter_ring_size">52dp</dimen>
|
||||
|
||||
<dimen name="contact_selection_actions_tap_area">10dp</dimen>
|
||||
|
@ -50,5 +54,6 @@
|
|||
<dimen name="slider_thumbOutlineSize">4dp</dimen>
|
||||
<dimen name="slider_displayTextFontSize">12sp</dimen>
|
||||
<dimen name="slider_displayTextBasicOffsetY">8dp</dimen>
|
||||
<dimen name="quote_thumb_size">45dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -709,5 +709,7 @@
|
|||
<string name="pref_reliable_service_explain">Requires a permanent notification</string>
|
||||
<string name="perm_enable_bg_reminder_title">Tap here to receive messages while Delta Chat is in the background.</string>
|
||||
<string name="perm_enable_bg_already_done">You already allowed Delta Chat to receive messages in the background.\n\nIf messages still do not arrive in background, please also check your system settings.</string>
|
||||
<string name="ConversationFragment_quoted_message_not_found">Original message not found</string>
|
||||
<string name="reply_privately">Reply privately</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
<item name="android:fontFamily">sans-serif</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Text.Quote" parent="Base.TextAppearance.AppCompat.Small">
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Text.Caption" parent="Base.TextAppearance.AppCompat.Caption">
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
|
|
|
@ -178,6 +178,8 @@
|
|||
<item name="icon_tint">@color/grey_700</item>
|
||||
<item name="icon_tint_dark">@color/grey_100</item>
|
||||
|
||||
<item name="quote_dismiss_button_tint">@color/gray70</item>
|
||||
|
||||
<item name="group_members_dialog_icon">@drawable/ic_group_grey600_24dp</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
||||
|
||||
|
@ -323,6 +325,7 @@
|
|||
<item name="icon_tint">@color/grey_100</item>
|
||||
<item name="icon_tint_dark">?icon_tint</item>
|
||||
|
||||
<item name="quote_dismiss_button_tint">@color/core_white</item>
|
||||
|
||||
<item name="group_members_dialog_icon">@drawable/ic_group_white_24dp</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.b44t.messenger;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.util.Log;
|
||||
|
||||
public class DcContact {
|
||||
|
@ -60,4 +61,9 @@ public class DcContact {
|
|||
// working with raw c-data
|
||||
private long contactCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefContactCPtr();
|
||||
|
||||
public int getArgbColor() {
|
||||
int rgb = getColor();
|
||||
return Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,13 @@ public class DcMsg {
|
|||
public native void setDimension (int width, int height);
|
||||
public native void setDuration (int duration);
|
||||
public native void setLocation (float latitude, float longitude);
|
||||
public void setQuote (DcMsg quote) { setQuoteCPtr(quote.msgCPtr); }
|
||||
public native String getQuotedText ();
|
||||
|
||||
public DcMsg getQuotedMsg () {
|
||||
long cPtr = getQuotedMsgCPtr();
|
||||
return cPtr != 0 ? new DcMsg(cPtr) : null;
|
||||
}
|
||||
|
||||
public File getFileAsFile() {
|
||||
if(getFile()==null)
|
||||
|
@ -189,4 +196,6 @@ public class DcMsg {
|
|||
private long msgCPtr; // CAVE: the name is referenced in the JNI
|
||||
private native void unrefMsgCPtr ();
|
||||
private native long getSummaryCPtr (long chatCPtr);
|
||||
private native void setQuoteCPtr (long quoteCPtr);
|
||||
private native long getQuotedMsgCPtr ();
|
||||
};
|
||||
|
|
|
@ -27,5 +27,6 @@ public interface BindableConversationItem extends Unbindable {
|
|||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(DcMsg messageRecord);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide;
|
|||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
|
@ -110,6 +111,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
|||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
|
||||
import org.thoughtcrime.securesms.videochat.VideochatUtil;
|
||||
|
@ -154,6 +156,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
public static final String TEXT_EXTRA = "draft_text";
|
||||
public static final String STARTING_POSITION_EXTRA = "starting_position";
|
||||
public static final String SCROLL_TO_MSG_ID_EXTRA = "scroll_to_msg_id_extra";
|
||||
|
||||
private static final int PICK_GALLERY = 1;
|
||||
private static final int PICK_DOCUMENT = 2;
|
||||
|
@ -765,6 +768,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
composeText.setSelection(composeText.getText().length());
|
||||
}
|
||||
|
||||
DcMsg quote = draft.getQuotedMsg();
|
||||
if (quote != null) {
|
||||
handleReplyMessage(quote);
|
||||
}
|
||||
|
||||
String filename = draft.getFile();
|
||||
if (filename.isEmpty() || !new File(filename).exists()) {
|
||||
future.set(!text.isEmpty());
|
||||
|
@ -1047,12 +1055,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
final SettableFuture<Integer> future = new SettableFuture<>();
|
||||
|
||||
DcMsg msg = null;
|
||||
Optional<QuoteModel> quote = inputPanel.getQuote();
|
||||
Integer recompress = 0;
|
||||
|
||||
// for a quick ui feedback, we clear the related controls immediately on sending messages.
|
||||
// for drafts, however, we do not change the controls, the activity may be resumed.
|
||||
if (action==ACTION_SEND_OUT) {
|
||||
composeText.setText("");
|
||||
inputPanel.clearQuote();
|
||||
}
|
||||
|
||||
if(slideDeck!=null) {
|
||||
|
@ -1095,6 +1105,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
msg.setText(body);
|
||||
}
|
||||
|
||||
if (quote.isPresent()) {
|
||||
if (msg == null) msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
|
||||
msg.setQuote(quote.get().getQuotedMsg());
|
||||
}
|
||||
|
||||
// msg may still be null to clear drafts
|
||||
new AsyncTask<Object, Void, Void>() {
|
||||
@Override
|
||||
|
@ -1440,7 +1455,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void handleReplyMessage(DcMsg messageRecord) {
|
||||
public void handleReplyMessage(DcMsg msg) {
|
||||
// If you modify these lines you may also want to modify ConversationItem.setQuote():
|
||||
Recipient author = dcContext.getRecipient(dcContext.getContact(msg.getFromId()));
|
||||
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
if (msg.getType() != DcMsg.DC_MSG_TEXT) {
|
||||
slideDeck.addSlide(MediaUtil.getSlideForMsg(this, msg));
|
||||
}
|
||||
|
||||
String text = msg.getSummarytext(500);
|
||||
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
msg,
|
||||
msg.getTimestamp(),
|
||||
author,
|
||||
text,
|
||||
slideDeck);
|
||||
|
||||
inputPanel.clickOnComposeInput();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -21,6 +21,8 @@ import androidx.annotation.LayoutRes;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -49,6 +51,8 @@ import java.util.Locale;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.thoughtcrime.securesms.ConversationItem.PULSE_HIGHLIGHT_MILLIS;
|
||||
|
||||
/**
|
||||
* A DC adapter for a conversation thread. Ultimately
|
||||
* used by ConversationActivity to display a conversation
|
||||
|
@ -92,6 +96,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
private @NonNull DcChat dcChat;
|
||||
private @NonNull int[] dcMsgList = new int[0];
|
||||
private int positionToPulseHighlight = -1;
|
||||
private int positionCurrentlyPulseHighlighting = -1;
|
||||
private long pulseHighlightingSince = -1;
|
||||
private int lastSeenPosition = -1;
|
||||
private long lastSeen = -1;
|
||||
|
||||
|
@ -225,12 +231,24 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
ConversationAdapter.ViewHolder holder = (ConversationAdapter.ViewHolder)viewHolder;
|
||||
boolean pulseHighlight = position == positionToPulseHighlight;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (position == positionToPulseHighlight) {
|
||||
positionToPulseHighlight = -1;
|
||||
positionCurrentlyPulseHighlighting = position;
|
||||
pulseHighlightingSince = now;
|
||||
}
|
||||
|
||||
long elapsed = now - pulseHighlightingSince;
|
||||
boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
|
||||
|
||||
holder.getItem().bind(getMsg(position), dcChat, glideRequests, locale, batchSelected, recipient, pulseHighlight);
|
||||
}
|
||||
|
||||
if (pulseHighlight) {
|
||||
positionToPulseHighlight = -1;
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (viewHolder.itemView instanceof ConversationItem) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
|||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
||||
import org.thoughtcrime.securesms.videochat.VideochatUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -106,6 +107,7 @@ public class ConversationFragment extends Fragment
|
|||
private Recipient recipient;
|
||||
private long chatId;
|
||||
private int startingPosition;
|
||||
private int startingMsgId;
|
||||
private boolean firstLoad;
|
||||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
|
@ -160,6 +162,12 @@ public class ConversationFragment extends Fragment
|
|||
list.setLayoutManager(layoutManager);
|
||||
list.setItemAnimator(null);
|
||||
|
||||
new ConversationItemSwipeCallback(
|
||||
msg -> actionMode == null &&
|
||||
dcContext.getChat(msg.getChatId()).canSend(),
|
||||
this::handleReplyMessage
|
||||
).attachToRecyclerView(list);
|
||||
|
||||
// setLayerType() is needed to allow larger items (long texts in our case)
|
||||
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
|
||||
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
|
@ -282,6 +290,7 @@ public class ConversationFragment extends Fragment
|
|||
this.chatId = this.getActivity().getIntent().getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
|
||||
this.recipient = Recipient.from(getActivity(), Address.fromChat((int)this.chatId));
|
||||
this.startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
|
||||
this.startingMsgId = this.getActivity().getIntent().getIntExtra(ConversationActivity.SCROLL_TO_MSG_ID_EXTRA, -1);
|
||||
this.firstLoad = true;
|
||||
|
||||
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
|
||||
|
@ -311,10 +320,16 @@ public class ConversationFragment extends Fragment
|
|||
if (messageRecords.size() > 1) {
|
||||
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_reply).setVisible(false);
|
||||
menu.findItem(R.id.menu_context_reply_privately).setVisible(false);
|
||||
} else {
|
||||
DcMsg messageRecord = messageRecords.iterator().next();
|
||||
DcChat chat = getListAdapter().getChat();
|
||||
menu.findItem(R.id.menu_context_details).setVisible(true);
|
||||
menu.findItem(R.id.menu_context_save_attachment).setVisible(messageRecord.hasFile());
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(chat.canSend());
|
||||
boolean showReplyPrivately = chat.isGroup() && !messageRecord.isOutgoing();
|
||||
menu.findItem(R.id.menu_context_reply_privately).setVisible(showReplyPrivately);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -455,10 +470,33 @@ public class ConversationFragment extends Fragment
|
|||
private void handleResendMessage(final DcMsg message) {
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void handleReplyMessage(final DcMsg message) {
|
||||
if (getActivity() != null) {
|
||||
//noinspection ConstantConditions
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
|
||||
}
|
||||
|
||||
listener.handleReplyMessage(message);
|
||||
}
|
||||
|
||||
private void handleReplyMessagePrivately(final DcMsg msg) {
|
||||
|
||||
if (getActivity() != null) {
|
||||
int privateChatId = dcContext.createChatByContactId(msg.getFromId());
|
||||
DcMsg replyMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
|
||||
replyMsg.setQuote(msg);
|
||||
dcContext.setDraft(privateChatId, replyMsg);
|
||||
|
||||
Intent intent = new Intent(getActivity(), ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, privateChatId);
|
||||
getActivity().startActivity(intent);
|
||||
getActivity().finish();
|
||||
} else {
|
||||
Log.e(TAG, "Activity was null");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSaveAttachment(final DcMsg message) {
|
||||
SaveAttachmentTask.showWarningDialog(getContext(), (dialogInterface, i) -> {
|
||||
Permissions.with(getActivity())
|
||||
|
@ -492,13 +530,19 @@ public class ConversationFragment extends Fragment
|
|||
pixelOffset = (firstView == null) ? 0 : list.getBottom() - firstView.getBottom() - list.getPaddingBottom();
|
||||
}
|
||||
|
||||
if (getContext() == null) {
|
||||
Log.e(TAG, "reloadList: getContext() was null");
|
||||
return;
|
||||
}
|
||||
int[] msgs = DcHelper.getContext(getContext()).getChatMsgs((int) chatId, 0, 0);
|
||||
adapter.changeData(msgs);
|
||||
int lastSeenPosition = adapter.getLastSeenPosition();
|
||||
|
||||
if (firstLoad) {
|
||||
if (startingPosition >= 0) {
|
||||
scrollToStartingPosition(startingPosition);
|
||||
scrollAndHighlight(startingPosition, false);
|
||||
} else if (startingMsgId >= 0) {
|
||||
scrollToMsgId(startingMsgId);
|
||||
} else {
|
||||
scrollToLastSeenPosition(lastSeenPosition);
|
||||
}
|
||||
|
@ -529,10 +573,14 @@ public class ConversationFragment extends Fragment
|
|||
floatingLocationButton.setVisibility(dcContext.isSendingLocationsToChat((int) chatId)? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void scrollToStartingPosition(final int startingPosition) {
|
||||
private void scrollAndHighlight(final int pos, boolean smooth) {
|
||||
list.post(() -> {
|
||||
list.getLayoutManager().scrollToPosition(startingPosition);
|
||||
getListAdapter().pulseHighlightItem(startingPosition);
|
||||
if (smooth) {
|
||||
list.smoothScrollToPosition(pos);
|
||||
} else {
|
||||
list.scrollToPosition(pos);
|
||||
}
|
||||
getListAdapter().pulseHighlightItem(pos);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -546,7 +594,9 @@ public class ConversationFragment extends Fragment
|
|||
ConversationAdapter adapter = (ConversationAdapter)list.getAdapter();
|
||||
int position = adapter.msgIdToPosition(msgId);
|
||||
if (position!=-1) {
|
||||
scrollToStartingPosition(position);
|
||||
scrollAndHighlight(position, false);
|
||||
} else {
|
||||
Log.e(TAG, "msgId {} not found for scrolling");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -735,7 +785,9 @@ public class ConversationFragment extends Fragment
|
|||
if (getListAdapter().getSelectedItems().size() == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
setCorrectMenuVisibility(actionMode.getMenu());
|
||||
Menu menu = actionMode.getMenu();
|
||||
setCorrectMenuVisibility(menu);
|
||||
AdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
|
||||
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size()));
|
||||
}
|
||||
}
|
||||
|
@ -768,6 +820,48 @@ public class ConversationFragment extends Fragment
|
|||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuoteClicked(DcMsg messageRecord) {
|
||||
DcMsg quoted = messageRecord.getQuotedMsg();
|
||||
if (quoted == null) {
|
||||
Log.i(TAG, "Clicked on a quote whose original message we never had.");
|
||||
Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
int foreignChatId = quoted.getChatId();
|
||||
if (foreignChatId != 0 && foreignChatId != chatId) {
|
||||
Intent intent = new Intent(getActivity(), ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId);
|
||||
intent.putExtra(ConversationActivity.SCROLL_TO_MSG_ID_EXTRA, quoted.getId());
|
||||
if (getActivity() != null) {
|
||||
getActivity().startActivity(intent);
|
||||
getActivity().finish();
|
||||
} else {
|
||||
Log.e(TAG, "Activity was null");
|
||||
}
|
||||
} else {
|
||||
LinearLayoutManager layout = ((LinearLayoutManager) list.getLayoutManager());
|
||||
boolean smooth = false;
|
||||
ConversationAdapter adapter = (ConversationAdapter) list.getAdapter();
|
||||
if (adapter == null) return;
|
||||
int position = adapter.msgIdToPosition(quoted.getId());
|
||||
if (layout != null) {
|
||||
int distance1 = Math.abs(position - layout.findFirstVisibleItemPosition());
|
||||
int distance2 = Math.abs(position - layout.findLastVisibleItemPosition());
|
||||
int distance = Math.min(distance1, distance2);
|
||||
smooth = distance < 15;
|
||||
Log.i(TAG, "Scrolling to quote, smoth: " + smooth + ", distance: " + distance);
|
||||
}
|
||||
|
||||
if (position != -1) {
|
||||
scrollAndHighlight(position, smooth);
|
||||
} else {
|
||||
Log.e(TAG, "msgId {} not found for scrolling");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -799,6 +893,7 @@ public class ConversationFragment extends Fragment
|
|||
}
|
||||
|
||||
setCorrectMenuVisibility(menu);
|
||||
AdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -848,6 +943,9 @@ public class ConversationFragment extends Fragment
|
|||
handleReplyMessage(getSelectedMessageRecord());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.menu_context_reply_privately:
|
||||
handleReplyMessagePrivately(getSelectedMessageRecord());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -25,12 +25,15 @@ import androidx.annotation.DimenRes;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -47,13 +50,16 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
|||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
||||
|
@ -81,8 +87,11 @@ public class ConversationItem extends LinearLayout
|
|||
{
|
||||
private static final String TAG = ConversationItem.class.getSimpleName();
|
||||
|
||||
private static final Rect SWIPE_RECT = new Rect();
|
||||
|
||||
private static final Pattern CMD_PATTERN = Pattern.compile("(?<=^|\\s)/[a-zA-Z][a-zA-Z@\\d_/.-]{0,254}");
|
||||
private static final int MAX_MEASURE_CALLS = 3;
|
||||
static long PULSE_HIGHLIGHT_MILLIS = 500;
|
||||
|
||||
private DcMsg messageRecord;
|
||||
private DcChat dcChat;
|
||||
|
@ -92,12 +101,14 @@ public class ConversationItem extends LinearLayout
|
|||
private GlideRequests glideRequests;
|
||||
|
||||
protected ViewGroup bodyBubble;
|
||||
protected View reply;
|
||||
@Nullable private QuoteView quoteView;
|
||||
private TextView bodyText;
|
||||
private ConversationItemFooter footer;
|
||||
private TextView groupSender;
|
||||
private View groupSenderHolder;
|
||||
private AvatarImageView contactPhoto;
|
||||
private ViewGroup contactPhotoHolder;
|
||||
protected ViewGroup contactPhotoHolder;
|
||||
private ViewGroup container;
|
||||
|
||||
private @NonNull Set<DcMsg> batchSelected = new HashSet<>();
|
||||
|
@ -107,6 +118,8 @@ public class ConversationItem extends LinearLayout
|
|||
private @NonNull Stub<DocumentView> documentViewStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
|
||||
private int measureCalls;
|
||||
|
||||
private int incomingBubbleColor;
|
||||
private int outgoingBubbleColor;
|
||||
private int forwardedTitleColor;
|
||||
|
@ -147,7 +160,9 @@ public class ConversationItem extends LinearLayout
|
|||
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
|
||||
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
|
||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.container = findViewById(R.id.container);
|
||||
this.reply = findViewById(R.id.reply_icon);
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
|
||||
|
@ -189,6 +204,8 @@ public class ConversationItem extends LinearLayout
|
|||
setAuthor(messageRecord, groupThread);
|
||||
setMessageSpacing(context);
|
||||
setFooter(messageRecord, locale);
|
||||
setQuote(messageRecord);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -197,6 +214,14 @@ public class ConversationItem extends LinearLayout
|
|||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public boolean disallowSwipe(float downX, float downY) {
|
||||
if (reply == null) return true;
|
||||
if (!hasAudio(messageRecord)) return false;
|
||||
|
||||
audioViewStub.get().getSeekBarGlobalVisibleRect(SWIPE_RECT);
|
||||
return SWIPE_RECT.contains((int) downX, (int) downY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
@ -204,6 +229,32 @@ public class ConversationItem extends LinearLayout
|
|||
if (isInEditMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean needsMeasure = false;
|
||||
|
||||
if (hasQuote(messageRecord)) {
|
||||
if (quoteView == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
int quoteWidth = quoteView.getMeasuredWidth();
|
||||
int availableWidth = getAvailableMessageBubbleWidth(quoteView);
|
||||
|
||||
if (quoteWidth != availableWidth) {
|
||||
quoteView.getLayoutParams().width = availableWidth;
|
||||
needsMeasure = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMeasure) {
|
||||
if (measureCalls < MAX_MEASURE_CALLS) {
|
||||
measureCalls++;
|
||||
measure(widthMeasureSpec, heightMeasureSpec);
|
||||
} else {
|
||||
Log.w(TAG, "Hit measure() cap of " + MAX_MEASURE_CALLS);
|
||||
}
|
||||
} else {
|
||||
measureCalls = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeAttributes() {
|
||||
|
@ -245,7 +296,7 @@ public class ConversationItem extends LinearLayout
|
|||
} else if (pulseHighlight) {
|
||||
setBackgroundResource(R.drawable.conversation_item_background_animated);
|
||||
setSelected(true);
|
||||
postDelayed(() -> setSelected(false), 1500);
|
||||
postDelayed(() -> setSelected(false), PULSE_HIGHLIGHT_MILLIS);
|
||||
} else {
|
||||
setSelected(false);
|
||||
}
|
||||
|
@ -273,6 +324,10 @@ public class ConversationItem extends LinearLayout
|
|||
return type==DcMsg.DC_MSG_AUDIO || type==DcMsg.DC_MSG_VOICE;
|
||||
}
|
||||
|
||||
private boolean hasQuote(DcMsg messageRecord) {
|
||||
return !"".equals(messageRecord.getQuotedText());
|
||||
}
|
||||
|
||||
private boolean hasThumbnail(DcMsg messageRecord) {
|
||||
int type = messageRecord.getType();
|
||||
return type==DcMsg.DC_MSG_GIF || type==DcMsg.DC_MSG_IMAGE || type==DcMsg.DC_MSG_VIDEO;
|
||||
|
@ -492,6 +547,54 @@ public class ConversationItem extends LinearLayout
|
|||
return messageBody;
|
||||
}
|
||||
|
||||
private void setQuote(@NonNull DcMsg current) {
|
||||
if (quoteView == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
String quoteTxt = current.getQuotedText();
|
||||
if (quoteTxt == null || quoteTxt.isEmpty()) {
|
||||
quoteView.dismiss();
|
||||
if (mediaThumbnailStub.resolved()) {
|
||||
ViewUtil.setTopMargin(mediaThumbnailStub.get(), 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
DcMsg msg = current.getQuotedMsg();
|
||||
|
||||
// If you modify these lines you may also want to modify ConversationActivity.handleReplyMessage():
|
||||
Recipient author = null;
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
if (msg != null) {
|
||||
author = dcContext.getRecipient(dcContext.getContact(msg.getFromId()));
|
||||
if (msg.getType() != DcMsg.DC_MSG_TEXT) {
|
||||
slideDeck.addSlide(MediaUtil.getSlideForMsg(context, msg));
|
||||
}
|
||||
}
|
||||
|
||||
quoteView.setQuote(GlideApp.with(this),
|
||||
msg,
|
||||
author,
|
||||
quoteTxt,
|
||||
slideDeck);
|
||||
|
||||
quoteView.setVisibility(View.VISIBLE);
|
||||
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
quoteView.setOnClickListener(view -> {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
eventListener.onQuoteClicked(current);
|
||||
} else {
|
||||
passthroughClickListener.onClick(view);
|
||||
}
|
||||
});
|
||||
|
||||
quoteView.setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
if (mediaThumbnailStub.resolved()) {
|
||||
ViewUtil.setTopMargin(mediaThumbnailStub.get(), readDimen(R.dimen.message_bubble_top_padding));
|
||||
}
|
||||
}
|
||||
|
||||
private void setGutterSizes(@NonNull DcMsg current, boolean isGroupThread) {
|
||||
if (isGroupThread && current.isOutgoing()) {
|
||||
ViewUtil.setLeftMargin(container, readDimen(R.dimen.conversation_group_left_gutter));
|
||||
|
@ -535,9 +638,7 @@ public class ConversationItem extends LinearLayout
|
|||
else if (groupThread && !messageRecord.isOutgoing() && dcContact !=null) {
|
||||
this.groupSender.setText(dcContact.getDisplayName());
|
||||
|
||||
int rgb = dcContact.getColor();
|
||||
int argb = Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb));
|
||||
this.groupSender.setTextColor(argb);
|
||||
this.groupSender.setTextColor(dcContact.getArgbColor());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -589,6 +690,21 @@ public class ConversationItem extends LinearLayout
|
|||
return context.getResources().getDimensionPixelOffset(dimenId);
|
||||
}
|
||||
|
||||
private int getAvailableMessageBubbleWidth(@NonNull View forView) {
|
||||
int availableWidth;
|
||||
if (hasAudio(messageRecord)) {
|
||||
availableWidth = audioViewStub.get().getMeasuredWidth() + ViewUtil.getLeftMargin(audioViewStub.get()) + ViewUtil.getRightMargin(audioViewStub.get());
|
||||
} else if (hasThumbnail(messageRecord)) {
|
||||
availableWidth = mediaThumbnailStub.get().getMeasuredWidth();
|
||||
} else {
|
||||
availableWidth = bodyBubble.getMeasuredWidth() - bodyBubble.getPaddingLeft() - bodyBubble.getPaddingRight();
|
||||
}
|
||||
|
||||
availableWidth -= ViewUtil.getLeftMargin(forView) + ViewUtil.getRightMargin(forView);
|
||||
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
/// Event handlers
|
||||
|
||||
private void handleDeadDropClick() {
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.os.Vibrator;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.util.AccessibilityUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
private static float SWIPE_SUCCESS_DX = ConversationSwipeAnimationHelper.TRIGGER_DX;
|
||||
private static long SWIPE_SUCCESS_VIBE_TIME_MS = 10;
|
||||
|
||||
private boolean swipeBack;
|
||||
private boolean shouldTriggerSwipeFeedback;
|
||||
private boolean canTriggerSwipe;
|
||||
private float latestDownX;
|
||||
private float latestDownY;
|
||||
|
||||
private final SwipeAvailabilityProvider swipeAvailabilityProvider;
|
||||
private final ConversationItemTouchListener itemTouchListener;
|
||||
private final OnSwipeListener onSwipeListener;
|
||||
|
||||
ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider,
|
||||
@NonNull OnSwipeListener onSwipeListener)
|
||||
{
|
||||
super(0, ItemTouchHelper.END);
|
||||
this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate);
|
||||
this.swipeAvailabilityProvider = swipeAvailabilityProvider;
|
||||
this.onSwipeListener = onSwipeListener;
|
||||
this.shouldTriggerSwipeFeedback = true;
|
||||
this.canTriggerSwipe = true;
|
||||
}
|
||||
|
||||
void attachToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
recyclerView.addOnItemTouchListener(itemTouchListener);
|
||||
new ItemTouchHelper(this).attachToRecyclerView(recyclerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull RecyclerView.ViewHolder target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSwipeDirs(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder)
|
||||
{
|
||||
if (cannotSwipeViewHolder(viewHolder)) return 0;
|
||||
return super.getSwipeDirs(recyclerView, viewHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
|
||||
if (swipeBack) {
|
||||
swipeBack = false;
|
||||
return 0;
|
||||
}
|
||||
return super.convertToAbsoluteDirection(flags, layoutDirection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(
|
||||
@NonNull Canvas c,
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dx, float dy, int actionState, boolean isCurrentlyActive)
|
||||
{
|
||||
if (cannotSwipeViewHolder(viewHolder)) return;
|
||||
|
||||
float sign = getSignFromDirection(viewHolder.itemView);
|
||||
boolean isCorrectSwipeDir = sameSign(dx, sign);
|
||||
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && isCorrectSwipeDir) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, Math.abs(dx), sign);
|
||||
handleSwipeFeedback((ConversationItem) viewHolder.itemView, Math.abs(dx));
|
||||
if (canTriggerSwipe) {
|
||||
setTouchListener(recyclerView, viewHolder, Math.abs(dx));
|
||||
}
|
||||
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE || dx == 0) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
|
||||
}
|
||||
|
||||
if (dx == 0) {
|
||||
shouldTriggerSwipeFeedback = true;
|
||||
canTriggerSwipe = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSwipeFeedback(@NonNull ConversationItem item, float dx) {
|
||||
if (dx > SWIPE_SUCCESS_DX && shouldTriggerSwipeFeedback) {
|
||||
vibrate(item.getContext());
|
||||
ConversationSwipeAnimationHelper.trigger(item);
|
||||
shouldTriggerSwipeFeedback = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (cannotSwipeViewHolder(viewHolder)) return;
|
||||
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
DcMsg messageRecord = item.getMessageRecord();
|
||||
|
||||
onSwipeListener.onSwipe(messageRecord);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void setTouchListener(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dx)
|
||||
{
|
||||
recyclerView.setOnTouchListener(new View.OnTouchListener() {
|
||||
|
||||
// This variable is necessary to make sure that the handleTouchActionUp() and therefore onSwiped() is called only once.
|
||||
// Otherwise, any subsequent little swipe would invoke onSwiped().
|
||||
// We can't call recyclerView.setOnTouchListener(null) because another ConversationItem might have set its own
|
||||
// on touch listener in the meantime and we don't want to cancel it
|
||||
private boolean listenerCalled = false;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
shouldTriggerSwipeFeedback = true;
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (!listenerCalled) {
|
||||
listenerCalled = true;
|
||||
ConversationItemSwipeCallback.this.handleTouchActionUp(recyclerView, viewHolder, dx);
|
||||
}
|
||||
//fallthrough
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
swipeBack = true;
|
||||
shouldTriggerSwipeFeedback = false;
|
||||
// Sometimes the view does not go back correctly, so make sure that after 2s the progress is reset:
|
||||
viewHolder.itemView.postDelayed(() -> resetProgress(viewHolder), 2000);
|
||||
if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) {
|
||||
resetProgress(viewHolder);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void handleTouchActionUp(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dx)
|
||||
{
|
||||
if (dx > SWIPE_SUCCESS_DX) {
|
||||
canTriggerSwipe = false;
|
||||
onSwiped(viewHolder);
|
||||
if (shouldTriggerSwipeFeedback) {
|
||||
vibrate(viewHolder.itemView.getContext());
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
recyclerView.cancelPendingInputEvents();
|
||||
}
|
||||
}
|
||||
|
||||
private static void resetProgress(RecyclerView.ViewHolder viewHolder) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView,
|
||||
0f,
|
||||
getSignFromDirection(viewHolder.itemView));
|
||||
}
|
||||
|
||||
private boolean cannotSwipeViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (!(viewHolder.itemView instanceof ConversationItem)) return true;
|
||||
|
||||
ConversationItem item = ((ConversationItem) viewHolder.itemView);
|
||||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) ||
|
||||
item.disallowSwipe(latestDownX, latestDownY);
|
||||
}
|
||||
|
||||
private void updateLatestDownCoordinate(float x, float y) {
|
||||
latestDownX = x;
|
||||
latestDownY = y;
|
||||
}
|
||||
|
||||
private static float getSignFromDirection(@NonNull View view) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -1f : 1f;
|
||||
} else {
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameSign(float dX, float sign) {
|
||||
return dX * sign > 0;
|
||||
}
|
||||
|
||||
private static void vibrate(@NonNull Context context) {
|
||||
Vibrator vibrator = ServiceUtil.getVibrator(context);
|
||||
if (vibrator != null) vibrator.vibrate(SWIPE_SUCCESS_VIBE_TIME_MS);
|
||||
}
|
||||
|
||||
interface SwipeAvailabilityProvider {
|
||||
boolean isSwipeAvailable(DcMsg conversationMessage);
|
||||
}
|
||||
|
||||
interface OnSwipeListener {
|
||||
void onSwipe(DcMsg conversationMessage);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
final class ConversationItemTouchListener extends RecyclerView.SimpleOnItemTouchListener {
|
||||
|
||||
private final Callback callback;
|
||||
|
||||
ConversationItemTouchListener(Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
if (e.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
callback.onDownEvent(e.getRawX(), e.getRawY());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
void onDownEvent(float rawX, float rawY);
|
||||
}
|
||||
}
|
|
@ -88,7 +88,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
|||
ApplicationDcContext dcContext = DcHelper.getContext(this);
|
||||
|
||||
// add welcome message
|
||||
dcContext.updateDeviceChats();
|
||||
// dcContext.updateDeviceChats();
|
||||
|
||||
// update messages - for new messages, do not reuse or modify strings but create new ones.
|
||||
// it is not needed to keep all past update messages, however, when deleted, also the strings should be deleted.
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.res.Resources;
|
||||
import android.view.View;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
final class ConversationSwipeAnimationHelper {
|
||||
|
||||
static final float TRIGGER_DX = dpToPx(64);
|
||||
static final float MAX_DX = dpToPx(96);
|
||||
|
||||
private static final float REPLY_SCALE_OVERSHOOT = 1.8f;
|
||||
private static final float REPLY_SCALE_MAX = 1.2f;
|
||||
private static final float REPLY_SCALE_MIN = 1f;
|
||||
private static final long REPLY_SCALE_OVERSHOOT_DURATION = 200;
|
||||
|
||||
private static final Interpolator BUBBLE_INTERPOLATOR = new BubblePositionInterpolator(0f, TRIGGER_DX, MAX_DX);
|
||||
private static final Interpolator REPLY_ALPHA_INTERPOLATOR = new ClampingLinearInterpolator(0f, 1f, 1f);
|
||||
private static final Interpolator REPLY_TRANSITION_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(10));
|
||||
private static final Interpolator AVATAR_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(8));
|
||||
private static final Interpolator REPLY_SCALE_INTERPOLATOR = new ClampingLinearInterpolator(REPLY_SCALE_MIN, REPLY_SCALE_MAX);
|
||||
|
||||
private ConversationSwipeAnimationHelper() {
|
||||
}
|
||||
|
||||
public static void update(@NonNull ConversationItem conversationItem, float dx, float sign) {
|
||||
float progress = dx / TRIGGER_DX;
|
||||
|
||||
updateBodyBubbleTransition(conversationItem.bodyBubble, dx, sign);
|
||||
updateReplyIconTransition(conversationItem.reply, dx, progress, sign);
|
||||
updateContactPhotoHolderTransition(conversationItem.contactPhotoHolder, progress, sign);
|
||||
}
|
||||
|
||||
public static void trigger(@NonNull ConversationItem conversationItem) {
|
||||
triggerReplyIcon(conversationItem.reply);
|
||||
}
|
||||
|
||||
private static void updateBodyBubbleTransition(@NonNull View bodyBubble, float dx, float sign) {
|
||||
bodyBubble.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign);
|
||||
}
|
||||
|
||||
private static void updateReactionsTransition(@NonNull View reactionsContainer, float dx, float sign) {
|
||||
reactionsContainer.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign);
|
||||
}
|
||||
|
||||
private static void updateReplyIconTransition(@NonNull View replyIcon, float dx, float progress, float sign) {
|
||||
if (progress > 0.05f) {
|
||||
replyIcon.setAlpha(REPLY_ALPHA_INTERPOLATOR.getInterpolation(progress));
|
||||
} else replyIcon.setAlpha(0f);
|
||||
|
||||
replyIcon.setTranslationX(REPLY_TRANSITION_INTERPOLATOR.getInterpolation(progress) * sign);
|
||||
|
||||
if (dx < TRIGGER_DX) {
|
||||
float scale = REPLY_SCALE_INTERPOLATOR.getInterpolation(progress);
|
||||
replyIcon.setScaleX(scale);
|
||||
replyIcon.setScaleY(scale);
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateContactPhotoHolderTransition(@Nullable View contactPhotoHolder,
|
||||
float progress,
|
||||
float sign)
|
||||
{
|
||||
if (contactPhotoHolder == null) return;
|
||||
contactPhotoHolder.setTranslationX(AVATAR_INTERPOLATOR.getInterpolation(progress) * sign);
|
||||
}
|
||||
|
||||
private static void triggerReplyIcon(@NonNull View replyIcon) {
|
||||
ValueAnimator animator = ValueAnimator.ofFloat(REPLY_SCALE_MAX, REPLY_SCALE_OVERSHOOT, REPLY_SCALE_MAX);
|
||||
animator.setDuration(REPLY_SCALE_OVERSHOOT_DURATION);
|
||||
animator.addUpdateListener(animation -> {
|
||||
replyIcon.setScaleX((float) animation.getAnimatedValue());
|
||||
replyIcon.setScaleY((float) animation.getAnimatedValue());
|
||||
});
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private static int dpToPx(int dp) {
|
||||
return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
private static final class BubblePositionInterpolator implements Interpolator {
|
||||
|
||||
private final float start;
|
||||
private final float middle;
|
||||
private final float end;
|
||||
|
||||
private BubblePositionInterpolator(float start, float middle, float end) {
|
||||
this.start = start;
|
||||
this.middle = middle;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getInterpolation(float input) {
|
||||
if (input < start) {
|
||||
return start;
|
||||
} else if (input < middle) {
|
||||
return input;
|
||||
} else {
|
||||
float segmentLength = end - middle;
|
||||
float segmentTraveled = input - middle;
|
||||
float segmentCompletion = segmentTraveled / segmentLength;
|
||||
float scaleDownFactor = middle / (input * 2);
|
||||
float output = middle + (segmentLength * segmentCompletion * scaleDownFactor);
|
||||
|
||||
return Math.min(output, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ClampingLinearInterpolator implements Interpolator {
|
||||
|
||||
private final float slope;
|
||||
private final float yIntercept;
|
||||
private final float max;
|
||||
private final float min;
|
||||
|
||||
ClampingLinearInterpolator(float start, float end) {
|
||||
this(start, end, 1.0f);
|
||||
}
|
||||
|
||||
ClampingLinearInterpolator(float start, float end, float scale) {
|
||||
slope = (end - start) * scale;
|
||||
yIntercept = start;
|
||||
max = Math.max(start, end);
|
||||
min = Math.min(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getInterpolation(float input) {
|
||||
return Util.clamp(slope * input + yIntercept, min, max);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.annotation.TargetApi;
|
|||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -182,6 +183,10 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener
|
|||
}
|
||||
}
|
||||
|
||||
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
|
||||
seekBar.getGlobalVisibleRect(rect);
|
||||
}
|
||||
|
||||
private double getProgress() {
|
||||
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
|
||||
return 0;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
@ -9,6 +11,7 @@ import android.util.AttributeSet;
|
|||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
|
@ -19,23 +22,32 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class InputPanel extends LinearLayout
|
||||
public class InputPanel extends ConstraintLayout
|
||||
implements MicrophoneRecorderView.Listener,
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
EmojiKeyboardProvider.EmojiEventListener
|
||||
|
@ -45,6 +57,7 @@ public class InputPanel extends LinearLayout
|
|||
|
||||
private static final int FADE_TIME = 150;
|
||||
|
||||
private QuoteView quoteView;
|
||||
private EmojiToggle mediaKeyboard;
|
||||
private ComposeText composeText;
|
||||
private View quickCameraToggle;
|
||||
|
@ -55,6 +68,7 @@ public class InputPanel extends LinearLayout
|
|||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
private RecordTime recordTime;
|
||||
private ValueAnimator quoteAnimator;
|
||||
|
||||
private @Nullable Listener listener;
|
||||
private boolean emojiVisible;
|
||||
|
@ -76,7 +90,9 @@ public class InputPanel extends LinearLayout
|
|||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
View quoteDismiss = findViewById(R.id.quote_dismiss);
|
||||
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.mediaKeyboard = findViewById(R.id.emoji_toggle);
|
||||
this.composeText = findViewById(R.id.embedded_text_editor);
|
||||
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
||||
|
@ -101,6 +117,8 @@ public class InputPanel extends LinearLayout
|
|||
mediaKeyboard.setVisibility(View.VISIBLE);
|
||||
emojiVisible = true;
|
||||
}
|
||||
|
||||
quoteDismiss.setOnClickListener(v -> clearQuote());
|
||||
}
|
||||
|
||||
public void setListener(final @NonNull Listener listener) {
|
||||
|
@ -113,6 +131,81 @@ public class InputPanel extends LinearLayout
|
|||
composeText.setMediaListener(listener);
|
||||
}
|
||||
|
||||
public void setQuote(@NonNull GlideRequests glideRequests,
|
||||
@NonNull DcMsg msg,
|
||||
long id,
|
||||
@NonNull Recipient author,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull SlideDeck attachments)
|
||||
{
|
||||
this.quoteView.setQuote(glideRequests, msg, author, body, attachments);
|
||||
|
||||
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
|
||||
: 0;
|
||||
|
||||
this.quoteView.setVisibility(VISIBLE);
|
||||
this.quoteView.measure(0, 0);
|
||||
|
||||
if (quoteAnimator != null) {
|
||||
quoteAnimator.cancel();
|
||||
}
|
||||
|
||||
quoteAnimator = createHeightAnimator(quoteView, originalHeight, this.quoteView.getMeasuredHeight(), null);
|
||||
|
||||
quoteAnimator.start();
|
||||
}
|
||||
|
||||
public void clearQuote() {
|
||||
if (quoteAnimator != null) {
|
||||
quoteAnimator.cancel();
|
||||
}
|
||||
|
||||
quoteAnimator = createHeightAnimator(quoteView, quoteView.getMeasuredHeight(), 0, new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
quoteView.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
quoteAnimator.start();
|
||||
}
|
||||
|
||||
private static ValueAnimator createHeightAnimator(@NonNull View view,
|
||||
int originalHeight,
|
||||
int finalHeight,
|
||||
@Nullable AnimationCompleteListener onAnimationComplete)
|
||||
{
|
||||
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, finalHeight)
|
||||
.setDuration(300);
|
||||
|
||||
animator.addUpdateListener(animation -> {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
params.height = Math.max(1, (int) animation.getAnimatedValue());
|
||||
view.setLayoutParams(params);
|
||||
});
|
||||
|
||||
if (onAnimationComplete != null) {
|
||||
animator.addListener(onAnimationComplete);
|
||||
}
|
||||
|
||||
return animator;
|
||||
}
|
||||
|
||||
public Optional<QuoteModel> getQuote() {
|
||||
if (quoteView.getVisibility() == View.VISIBLE && quoteView.getBody() != null) {
|
||||
return Optional.of(new QuoteModel(
|
||||
quoteView.getDcContact(), quoteView.getBody().toString(),
|
||||
false, quoteView.getAttachments(), quoteView.getOriginalMsg()
|
||||
));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public void clickOnComposeInput() {
|
||||
composeText.performClick();
|
||||
}
|
||||
|
||||
public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) {
|
||||
this.mediaKeyboard.attach(mediaKeyboard);
|
||||
}
|
||||
|
|
260
src/org/thoughtcrime/securesms/components/QuoteView.java
Normal file
260
src/org/thoughtcrime/securesms/components/QuoteView.java
Normal file
|
@ -0,0 +1,260 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
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 ViewGroup mainView;
|
||||
private TextView authorView;
|
||||
private TextView bodyView;
|
||||
private ImageView quoteBarView;
|
||||
private ImageView thumbnailView;
|
||||
private View attachmentVideoOverlayView;
|
||||
private ViewGroup attachmentContainerView;
|
||||
private ImageView dismissView;
|
||||
|
||||
private DcMsg quotedMsg;
|
||||
private DcContact author;
|
||||
private CharSequence body;
|
||||
//private TextView missingLinkText;
|
||||
private SlideDeck attachments;
|
||||
private int messageType;
|
||||
private int largeCornerRadius;
|
||||
private int smallCornerRadius;
|
||||
// private CornerMask cornerMask;
|
||||
|
||||
|
||||
public QuoteView(Context context) {
|
||||
super(context);
|
||||
initialize(null);
|
||||
}
|
||||
|
||||
public QuoteView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
public QuoteView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
private void initialize(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.quote_view, this);
|
||||
|
||||
this.mainView = findViewById(R.id.quote_main);
|
||||
this.authorView = findViewById(R.id.quote_author);
|
||||
this.bodyView = findViewById(R.id.quote_text);
|
||||
this.quoteBarView = findViewById(R.id.quote_bar);
|
||||
this.thumbnailView = findViewById(R.id.quote_thumbnail);
|
||||
this.attachmentVideoOverlayView = findViewById(R.id.quote_video_overlay);
|
||||
this.attachmentContainerView = findViewById(R.id.quote_attachment_container);
|
||||
this.dismissView = findViewById(R.id.quote_dismiss);
|
||||
// this.largeCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_large);
|
||||
// this.smallCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
|
||||
|
||||
// cornerMask = new CornerMask(this);
|
||||
// cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
|
||||
int primaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorPrimary, Color.BLACK);
|
||||
int secondaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorSecondary, Color.BLACK);
|
||||
messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0);
|
||||
typedArray.recycle();
|
||||
|
||||
dismissView.setVisibility(messageType == MESSAGE_TYPE_PREVIEW ? VISIBLE : GONE);
|
||||
|
||||
// if (messageType == MESSAGE_TYPE_PREVIEW) {
|
||||
// int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
|
||||
// cornerMask.setTopLeftRadius(radius);
|
||||
// cornerMask.setTopRightRadius(radius);
|
||||
// }
|
||||
}
|
||||
|
||||
dismissView.setOnClickListener(view -> setVisibility(GONE));
|
||||
}
|
||||
//
|
||||
// @Override
|
||||
// protected void dispatchDraw(Canvas canvas) {
|
||||
// super.dispatchDraw(canvas);
|
||||
// cornerMask.mask(canvas);
|
||||
// }
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
//if (author != null) author.removeForeverObserver(this);
|
||||
}
|
||||
|
||||
public void setQuote(GlideRequests glideRequests,
|
||||
DcMsg msg,
|
||||
@Nullable Recipient author,
|
||||
@Nullable CharSequence body,
|
||||
@NonNull SlideDeck attachments)
|
||||
{
|
||||
// if (this.author != null) this.author.removeForeverObserver(this);
|
||||
|
||||
quotedMsg = msg;
|
||||
this.author = author != null ? author.getDcContact() : null;
|
||||
this.body = body;
|
||||
this.attachments = attachments;
|
||||
|
||||
//this.author.observeForever(this);
|
||||
setQuoteAuthor(author);
|
||||
setQuoteText(body, attachments);
|
||||
setQuoteAttachment(glideRequests, attachments);
|
||||
//setQuoteMissingFooter(originalMissing);
|
||||
}
|
||||
|
||||
// public void setTopCornerSizes(boolean topLeftLarge, boolean topRightLarge) {
|
||||
// cornerMask.setTopLeftRadius(topLeftLarge ? largeCornerRadius : smallCornerRadius);
|
||||
// cornerMask.setTopRightRadius(topRightLarge ? largeCornerRadius : smallCornerRadius);
|
||||
// }
|
||||
|
||||
public void dismiss() {
|
||||
//if (this.author != null) this.author.removeForeverObserver(this);
|
||||
|
||||
this.author = null;
|
||||
this.body = null;
|
||||
|
||||
setVisibility(GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
setQuoteAuthor(recipient);
|
||||
}
|
||||
|
||||
private void setQuoteAuthor(@Nullable Recipient author) {
|
||||
if (author == null) {
|
||||
authorView.setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
DcContact contact = author.getDcContact();
|
||||
if (contact != null) {
|
||||
authorView.setVisibility(VISIBLE);
|
||||
authorView.setText(contact.getDisplayName());
|
||||
quoteBarView.setBackgroundColor(contact.getArgbColor());
|
||||
authorView.setTextColor(contact.getArgbColor());
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
||||
bodyView.setVisibility(VISIBLE);
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
} else {
|
||||
bodyView.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
|
||||
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo()).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();
|
||||
|
||||
attachmentVideoOverlayView.setVisibility(GONE);
|
||||
|
||||
if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getUri() != null) {
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
dismissView.setBackgroundResource(R.drawable.dismiss_background);
|
||||
|
||||
Uri thumbnailUri = imageVideoSlides.get(0).getUri();
|
||||
if (imageVideoSlides.get(0).hasVideo()) {
|
||||
attachmentVideoOverlayView.setVisibility(VISIBLE);
|
||||
MediaUtil.createVideoThumbnailIfNeeded(getContext(), imageVideoSlides.get(0).getUri(), imageVideoSlides.get(0).getThumbnailUri(), null);
|
||||
thumbnailUri = imageVideoSlides.get(0).getThumbnailUri();
|
||||
}
|
||||
glideRequests.load(new DecryptableUri(thumbnailUri))
|
||||
.centerCrop()
|
||||
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
|
||||
} else if(!audioSlides.isEmpty()) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
} else if (!documentSlides.isEmpty()) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(VISIBLE);
|
||||
} else {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (ThemeUtil.isDarkTheme(getContext())) {
|
||||
dismissView.setBackgroundResource(R.drawable.circle_alpha);
|
||||
}
|
||||
}
|
||||
//
|
||||
// private void setQuoteMissingFooter(boolean missing) {
|
||||
// footerView.setVisibility(missing ? VISIBLE : GONE);
|
||||
// footerView.setBackgroundColor(author.getColor());
|
||||
// }
|
||||
|
||||
public CharSequence getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments.asAttachments();
|
||||
}
|
||||
|
||||
public DcContact getDcContact() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public DcMsg getOriginalMsg() {
|
||||
return quotedMsg;
|
||||
}
|
||||
}
|
|
@ -112,7 +112,7 @@ public class ScaleStableImageView
|
|||
large = storedSizes.get(landscapeWidth + "x" + landscapeHeight);
|
||||
if (large == null) return; // no baseline. can't work.
|
||||
Bitmap original = ((BitmapDrawable) large).getBitmap();
|
||||
if (height <= original.getHeight() || width <= original.getWidth()) {
|
||||
if (height <= original.getHeight() && width <= original.getWidth()) {
|
||||
Bitmap cropped = Bitmap.createBitmap(original, 0, 0, width, height);
|
||||
Drawable croppedDrawable = new BitmapDrawable(getResources(), cropped);
|
||||
overrideDrawable(croppedDrawable);
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
|
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
|||
|
||||
public class SearchToolbar extends LinearLayout {
|
||||
|
||||
private static final String TAG = SearchToolbar.class.getSimpleName();
|
||||
private float x, y;
|
||||
private MenuItem searchItem;
|
||||
private SearchListener listener;
|
||||
|
@ -47,6 +49,10 @@ public class SearchToolbar extends LinearLayout {
|
|||
setOrientation(VERTICAL);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
if (toolbar == null) {
|
||||
Log.e(TAG, "SearchToolbar: No toolbar");
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable drawable = getContext().getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp);
|
||||
drawable.mutate();
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.connect;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
|
@ -34,7 +36,7 @@ public class DcHelper {
|
|||
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
|
||||
public static final String CONFIG_WEBRTC_INSTANCE = "webrtc_instance";
|
||||
|
||||
public static ApplicationDcContext getContext(Context context) {
|
||||
public static ApplicationDcContext getContext(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).dcContext;
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ public class AudioSlide extends Slide {
|
|||
|
||||
@Override
|
||||
public boolean hasImage() {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
42
src/org/thoughtcrime/securesms/mms/QuoteModel.java
Normal file
42
src/org/thoughtcrime/securesms/mms/QuoteModel.java
Normal file
|
@ -0,0 +1,42 @@
|
|||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class QuoteModel {
|
||||
|
||||
private final DcContact author;
|
||||
private final String text;
|
||||
private final List<Attachment> attachments;
|
||||
private DcMsg quotedMsg;
|
||||
|
||||
public QuoteModel(DcContact author, String text, boolean missing, @Nullable List<Attachment> attachments, DcMsg quotedMsg) {
|
||||
this.author = author;
|
||||
this.text = text;
|
||||
this.attachments = attachments;
|
||||
this.quotedMsg = quotedMsg;
|
||||
}
|
||||
|
||||
public DcContact getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public DcMsg getQuotedMsg() {
|
||||
return quotedMsg;
|
||||
}
|
||||
}
|
|
@ -50,6 +50,19 @@ public class SlideDeck {
|
|||
slides.add(slide);
|
||||
}
|
||||
|
||||
public List<Slide> getSlides() {
|
||||
return slides;
|
||||
}
|
||||
|
||||
public boolean containsMediaSlide() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @Nullable DocumentSlide getDocumentSlide() {
|
||||
for (Slide slide: slides) {
|
||||
if (slide.hasDocument()) {
|
||||
|
|
|
@ -137,6 +137,10 @@ public class Recipient {
|
|||
return "";
|
||||
}
|
||||
|
||||
public @Nullable DcContact getDcContact() {
|
||||
return dcContact;
|
||||
}
|
||||
|
||||
public @NonNull Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface RecipientForeverObserver {
|
||||
@MainThread
|
||||
void onRecipientChanged(@NonNull Recipient recipient);
|
||||
}
|
19
src/org/thoughtcrime/securesms/util/AccessibilityUtil.java
Normal file
19
src/org/thoughtcrime/securesms/util/AccessibilityUtil.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
public final class AccessibilityUtil {
|
||||
|
||||
private AccessibilityUtil() {
|
||||
}
|
||||
|
||||
public static boolean areAnimationsDisabled(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
return Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1) == 0f;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.util;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
|
@ -240,4 +241,8 @@ public class ViewUtil {
|
|||
public static void setPaddingBottom(@NonNull View view, int padding) {
|
||||
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), padding);
|
||||
}
|
||||
|
||||
public static int dpToPx(int dp) {
|
||||
return Math.round(dp * Resources.getSystem().getDisplayMetrics().density);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package org.thoughtcrime.securesms.util.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* AdaptiveActionsToolbar behaves like a normal {@link Toolbar} except in that it ignores the
|
||||
* showAsAlways attributes of menu items added via menu inflation, opting for an adaptive algorithm
|
||||
* instead. This algorithm will display as many icons as it can up to a specific percentage of the
|
||||
* screen.
|
||||
*
|
||||
* Each ActionView icon is expected to occupy 48dp of space, including padding. Items are stacked one
|
||||
* after the next with no margins.
|
||||
*
|
||||
* This view can be customized via attributes:
|
||||
*
|
||||
* aat_max_shown -- controls the max number of items to display.
|
||||
* aat_percent_for_actions -- controls the max percent of screen width the buttons can occupy.
|
||||
*/
|
||||
public class AdaptiveActionsToolbar extends Toolbar {
|
||||
|
||||
private static final int NAVIGATION_DP = 56;
|
||||
private static final int ACTION_VIEW_WIDTH_DP = 48;
|
||||
private static final int OVERFLOW_VIEW_WIDTH_DP = 36;
|
||||
|
||||
private int maxShown;
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.toolbarStyle);
|
||||
}
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.AdaptiveActionsToolbar);
|
||||
|
||||
maxShown = array.getInteger(R.styleable.AdaptiveActionsToolbar_aat_max_shown, 100);
|
||||
|
||||
array.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
adjustMenuActions(getMenu(), maxShown, getMeasuredWidth());
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
public static void adjustMenuActions(@NonNull Menu menu, int maxToShow, int toolbarWidthPx) {
|
||||
int menuSize = 0;
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
if (menu.getItem(i).isVisible()) {
|
||||
menuSize++;
|
||||
}
|
||||
}
|
||||
|
||||
int widthAllowed = toolbarWidthPx - ViewUtil.dpToPx(NAVIGATION_DP);
|
||||
int nItemsToShow = Math.min(maxToShow, widthAllowed / ViewUtil.dpToPx(ACTION_VIEW_WIDTH_DP));
|
||||
|
||||
if (nItemsToShow < menuSize) {
|
||||
widthAllowed -= ViewUtil.dpToPx(OVERFLOW_VIEW_WIDTH_DP);
|
||||
}
|
||||
|
||||
nItemsToShow = Math.min(maxToShow, widthAllowed / ViewUtil.dpToPx(ACTION_VIEW_WIDTH_DP));
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
boolean neverShowAsAction = item.getItemId() == R.id.menu_context_reply_privately;
|
||||
if (item.isVisible() && nItemsToShow > 0 && !neverShowAsAction) {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
nItemsToShow--;
|
||||
} else {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue