mirror of
https://github.com/deltachat/deltachat-android.git
synced 2025-10-04 10:19:15 +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
|
* 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>
|
|
@ -1,21 +1,25 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<org.thoughtcrime.securesms.components.InputPanel
|
<org.thoughtcrime.securesms.components.InputPanel
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/bottom_panel"
|
android:id="@+id/bottom_panel"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal"
|
android:clickable="true"
|
||||||
android:clickable="true"
|
android:paddingTop="6dp"
|
||||||
android:paddingTop="6dp"
|
android:paddingBottom="6dp"
|
||||||
android:paddingBottom="6dp"
|
android:clipChildren="false"
|
||||||
android:clipChildren="false"
|
android:clipToPadding="false"
|
||||||
android:clipToPadding="false"
|
android:background="?attr/input_panel_bg_color">
|
||||||
android:background="?attr/input_panel_bg_color">
|
|
||||||
|
|
||||||
<FrameLayout
|
<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_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
@ -23,171 +27,157 @@
|
||||||
android:clipToPadding="false">
|
android:clipToPadding="false">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/recording_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false"
|
android:paddingLeft="12dp"
|
||||||
android:clipToPadding="false"
|
android:paddingRight="12dp"
|
||||||
android:orientation="vertical">
|
android:layout_gravity="center_vertical"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/record_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="none"
|
||||||
|
style="@style/Signal.Text.Body"
|
||||||
|
android:text="00:00.00"
|
||||||
|
android:textColor="?conversation_item_outgoing_text_primary_color"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slide_to_cancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="left|start|center_vertical"
|
||||||
|
android:drawableLeft="@drawable/slidearrow"
|
||||||
|
android:drawableStart="@drawable/slidearrow"
|
||||||
|
style="@style/Signal.Text.Preview"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="@string/chat_record_slide_to_cancel"
|
||||||
|
android:textColor="@color/slidearrow_color"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingRight="50dp"
|
||||||
|
android:paddingEnd="50dp"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
||||||
|
android:id="@+id/emoji_toggle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:paddingLeft="12dp"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingRight="6dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:background="@drawable/touch_highlight_background"
|
||||||
|
android:contentDescription="@string/menu_toggle_keyboard" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.ComposeText
|
||||||
|
style="@style/ComposeEditText"
|
||||||
|
android:id="@+id/embedded_text_editor"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="2dp"
|
||||||
|
android:layout_marginRight="2dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:nextFocusForward="@+id/send_button"
|
||||||
|
android:nextFocusRight="@+id/send_button"
|
||||||
|
tools:visibility="invisible"
|
||||||
|
tools:hint="Send message" >
|
||||||
|
<requestFocus />
|
||||||
|
</org.thoughtcrime.securesms.components.ComposeText>
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.HidingLinearLayout
|
||||||
|
android:id="@+id/quick_attachment_toggle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false">
|
android:clipToPadding="false">
|
||||||
|
|
||||||
<LinearLayout
|
<ImageButton
|
||||||
android:id="@+id/recording_container"
|
android:id="@+id/quick_camera_toggle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:paddingLeft="12dp"
|
|
||||||
android:paddingRight="12dp"
|
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:gravity="center_vertical"
|
android:src="?quick_camera_icon"
|
||||||
android:orientation="horizontal"
|
android:paddingLeft="11dp"
|
||||||
tools:visibility="gone">
|
android:paddingRight="11dp"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:background="@drawable/touch_highlight_background"
|
||||||
|
android:contentDescription="@string/camera" />
|
||||||
|
|
||||||
<TextView
|
<org.thoughtcrime.securesms.components.MicrophoneRecorderView
|
||||||
android:id="@+id/record_time"
|
android:id="@+id/recorder_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:ellipsize="none"
|
android:layout_gravity="center_vertical"
|
||||||
style="@style/Signal.Text.Body"
|
|
||||||
android:text="00:00.00"
|
|
||||||
android:textColor="?conversation_item_outgoing_text_primary_color"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible"/>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipChildren="true">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/slide_to_cancel"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="left|start|center_vertical"
|
|
||||||
android:drawableLeft="@drawable/slidearrow"
|
|
||||||
android:drawableStart="@drawable/slidearrow"
|
|
||||||
style="@style/Signal.Text.Preview"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/chat_record_slide_to_cancel"
|
|
||||||
android:textColor="@color/slidearrow_color"
|
|
||||||
android:ellipsize="none"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:paddingLeft="10dp"
|
|
||||||
android:paddingStart="10dp"
|
|
||||||
android:paddingRight="50dp"
|
|
||||||
android:paddingEnd="50dp"
|
|
||||||
tools:visibility="visible"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false">
|
android:clipToPadding="false">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
<ImageButton
|
||||||
android:id="@+id/emoji_toggle"
|
android:id="@+id/quick_audio_toggle"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:minHeight="40dp"
|
|
||||||
android:paddingLeft="12dp"
|
|
||||||
android:paddingStart="12dp"
|
|
||||||
android:paddingRight="6dp"
|
|
||||||
android:paddingEnd="6dp"
|
|
||||||
android:background="@drawable/touch_highlight_background"
|
|
||||||
android:contentDescription="@string/menu_toggle_keyboard" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ComposeText
|
|
||||||
style="@style/ComposeEditText"
|
|
||||||
android:id="@+id/embedded_text_editor"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="2dp"
|
|
||||||
android:layout_marginRight="2dp"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:nextFocusForward="@+id/send_button"
|
|
||||||
android:nextFocusRight="@+id/send_button"
|
|
||||||
tools:visibility="invisible"
|
|
||||||
tools:hint="Send message" >
|
|
||||||
<requestFocus />
|
|
||||||
</org.thoughtcrime.securesms.components.ComposeText>
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.HidingLinearLayout
|
|
||||||
android:id="@+id/quick_attachment_toggle"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:layout_gravity="center_vertical"
|
||||||
android:clipToPadding="false">
|
android:layout_marginRight="2dp"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
|
android:paddingLeft="11dp"
|
||||||
|
android:paddingRight="11dp"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:src="?quick_mic_icon"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="@string/audio" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageView
|
||||||
android:id="@+id/quick_camera_toggle"
|
android:id="@+id/quick_audio_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="74dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="74dp"
|
||||||
android:layout_gravity="center_vertical"
|
android:src="@drawable/ic_mic_white_48dp"
|
||||||
android:src="?quick_camera_icon"
|
android:background="@drawable/circle_tintable"
|
||||||
android:paddingLeft="11dp"
|
android:backgroundTint="@color/audio_icon"
|
||||||
android:paddingRight="11dp"
|
android:elevation="4dp"
|
||||||
android:paddingTop="6dp"
|
android:visibility="gone"
|
||||||
android:paddingBottom="6dp"
|
android:scaleType="center"/>
|
||||||
android:background="@drawable/touch_highlight_background"
|
|
||||||
android:contentDescription="@string/camera" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.MicrophoneRecorderView
|
</org.thoughtcrime.securesms.components.MicrophoneRecorderView>
|
||||||
android:id="@+id/recorder_view"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false">
|
|
||||||
|
|
||||||
<ImageButton
|
</org.thoughtcrime.securesms.components.HidingLinearLayout>
|
||||||
android:id="@+id/quick_audio_toggle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_marginRight="2dp"
|
|
||||||
android:layout_marginEnd="2dp"
|
|
||||||
android:paddingLeft="11dp"
|
|
||||||
android:paddingRight="11dp"
|
|
||||||
android:paddingTop="6dp"
|
|
||||||
android:paddingBottom="6dp"
|
|
||||||
android:src="?quick_mic_icon"
|
|
||||||
android:background="@null"
|
|
||||||
android:contentDescription="@string/audio" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/quick_audio_fab"
|
|
||||||
android:layout_width="74dp"
|
|
||||||
android:layout_height="74dp"
|
|
||||||
android:src="@drawable/ic_mic_white_48dp"
|
|
||||||
android:background="@drawable/circle_tintable"
|
|
||||||
android:backgroundTint="@color/audio_icon"
|
|
||||||
android:elevation="4dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:scaleType="center"/>
|
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.MicrophoneRecorderView>
|
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.HidingLinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
<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:id="@+id/button_toggle"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
@ -221,4 +211,15 @@
|
||||||
android:background="@drawable/circle_touch_highlight_background" />
|
android:background="@drawable/circle_touch_highlight_background" />
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
</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>
|
</org.thoughtcrime.securesms.components.InputPanel>
|
||||||
|
|
|
@ -25,6 +25,18 @@
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:clipChildren="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
|
<FrameLayout
|
||||||
android:id="@+id/contact_photo_container"
|
android:id="@+id/contact_photo_container"
|
||||||
android:layout_width="36dp"
|
android:layout_width="36dp"
|
||||||
|
@ -87,6 +99,19 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</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
|
<ViewStub
|
||||||
android:id="@+id/image_view_stub"
|
android:id="@+id/image_view_stub"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -23,6 +23,18 @@
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:clipChildren="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
|
<LinearLayout
|
||||||
android:id="@+id/body_bubble"
|
android:id="@+id/body_bubble"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -65,6 +77,19 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</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
|
<ViewStub
|
||||||
android:id="@+id/image_view_stub"
|
android:id="@+id/image_view_stub"
|
||||||
android:layout_width="wrap_content"
|
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"
|
<item android:title="@string/menu_reply"
|
||||||
android:id="@+id/menu_context_reply"
|
android:id="@+id/menu_context_reply"
|
||||||
android:visible="false"
|
|
||||||
android:icon="?menu_reply_icon"
|
android:icon="?menu_reply_icon"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
@ -37,4 +36,7 @@
|
||||||
android:icon="?menu_forward_icon"
|
android:icon="?menu_forward_icon"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
<item android:title="@string/reply_privately"
|
||||||
|
android:id="@+id/menu_context_reply_privately"
|
||||||
|
app:showAsAction="collapseActionView" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -119,6 +119,9 @@
|
||||||
|
|
||||||
<attr name="pref_icon_tint" format="color"/>
|
<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">
|
<declare-styleable name="CustomDefaultPreference">
|
||||||
<attr name="custom_pref_toggle" format="string"/>
|
<attr name="custom_pref_toggle" format="string"/>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
@ -236,4 +239,18 @@
|
||||||
<attr name="timeFrame" format="integer"/>
|
<attr name="timeFrame" format="integer"/>
|
||||||
</declare-styleable>
|
</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>
|
</resources>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<dimen name="message_bubble_shadow_distance">1.5dp</dimen>
|
<dimen name="message_bubble_shadow_distance">1.5dp</dimen>
|
||||||
<dimen name="message_bubble_horizontal_padding">8dp</dimen>
|
<dimen name="message_bubble_horizontal_padding">8dp</dimen>
|
||||||
<dimen name="message_bubble_horizontal_padding_half">4dp</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_collapsed_footer_padding">6dp</dimen>
|
||||||
<dimen name="message_bubble_edge_margin">32dp</dimen>
|
<dimen name="message_bubble_edge_margin">32dp</dimen>
|
||||||
<dimen name="message_bubble_bottom_padding">6dp</dimen>
|
<dimen name="message_bubble_bottom_padding">6dp</dimen>
|
||||||
|
@ -41,6 +41,10 @@
|
||||||
<dimen name="conversation_group_left_gutter">52dp</dimen>
|
<dimen name="conversation_group_left_gutter">52dp</dimen>
|
||||||
<dimen name="conversation_vertical_message_spacing_collapse">1dp</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="quick_camera_shutter_ring_size">52dp</dimen>
|
||||||
|
|
||||||
<dimen name="contact_selection_actions_tap_area">10dp</dimen>
|
<dimen name="contact_selection_actions_tap_area">10dp</dimen>
|
||||||
|
@ -50,5 +54,6 @@
|
||||||
<dimen name="slider_thumbOutlineSize">4dp</dimen>
|
<dimen name="slider_thumbOutlineSize">4dp</dimen>
|
||||||
<dimen name="slider_displayTextFontSize">12sp</dimen>
|
<dimen name="slider_displayTextFontSize">12sp</dimen>
|
||||||
<dimen name="slider_displayTextBasicOffsetY">8dp</dimen>
|
<dimen name="slider_displayTextBasicOffsetY">8dp</dimen>
|
||||||
|
<dimen name="quote_thumb_size">45dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -709,5 +709,7 @@
|
||||||
<string name="pref_reliable_service_explain">Requires a permanent notification</string>
|
<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_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="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>
|
</resources>
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
<item name="android:fontFamily">sans-serif</item>
|
||||||
</style>
|
</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">
|
<style name="Signal.Text.Caption" parent="Base.TextAppearance.AppCompat.Caption">
|
||||||
<item name="android:textSize">12sp</item>
|
<item name="android:textSize">12sp</item>
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
<item name="android:fontFamily">sans-serif</item>
|
||||||
|
|
|
@ -178,6 +178,8 @@
|
||||||
<item name="icon_tint">@color/grey_700</item>
|
<item name="icon_tint">@color/grey_700</item>
|
||||||
<item name="icon_tint_dark">@color/grey_100</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="group_members_dialog_icon">@drawable/ic_group_grey600_24dp</item>
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
||||||
|
|
||||||
|
@ -323,6 +325,7 @@
|
||||||
<item name="icon_tint">@color/grey_100</item>
|
<item name="icon_tint">@color/grey_100</item>
|
||||||
<item name="icon_tint_dark">?icon_tint</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="group_members_dialog_icon">@drawable/ic_group_white_24dp</item>
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix</item>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.b44t.messenger;
|
package com.b44t.messenger;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
public class DcContact {
|
public class DcContact {
|
||||||
|
@ -60,4 +61,9 @@ public class DcContact {
|
||||||
// working with raw c-data
|
// working with raw c-data
|
||||||
private long contactCPtr; // CAVE: the name is referenced in the JNI
|
private long contactCPtr; // CAVE: the name is referenced in the JNI
|
||||||
private native void unrefContactCPtr();
|
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 setDimension (int width, int height);
|
||||||
public native void setDuration (int duration);
|
public native void setDuration (int duration);
|
||||||
public native void setLocation (float latitude, float longitude);
|
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() {
|
public File getFileAsFile() {
|
||||||
if(getFile()==null)
|
if(getFile()==null)
|
||||||
|
@ -189,4 +196,6 @@ public class DcMsg {
|
||||||
private long msgCPtr; // CAVE: the name is referenced in the JNI
|
private long msgCPtr; // CAVE: the name is referenced in the JNI
|
||||||
private native void unrefMsgCPtr ();
|
private native void unrefMsgCPtr ();
|
||||||
private native long getSummaryCPtr (long chatCPtr);
|
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);
|
void setEventListener(@Nullable EventListener listener);
|
||||||
|
|
||||||
interface EventListener {
|
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.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
|
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
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.AssertedSuccessListener;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
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.util.views.Stub;
|
||||||
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
|
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
|
||||||
import org.thoughtcrime.securesms.videochat.VideochatUtil;
|
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 CHAT_ID_EXTRA = "chat_id";
|
||||||
public static final String TEXT_EXTRA = "draft_text";
|
public static final String TEXT_EXTRA = "draft_text";
|
||||||
public static final String STARTING_POSITION_EXTRA = "starting_position";
|
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_GALLERY = 1;
|
||||||
private static final int PICK_DOCUMENT = 2;
|
private static final int PICK_DOCUMENT = 2;
|
||||||
|
@ -765,6 +768,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
composeText.setSelection(composeText.getText().length());
|
composeText.setSelection(composeText.getText().length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DcMsg quote = draft.getQuotedMsg();
|
||||||
|
if (quote != null) {
|
||||||
|
handleReplyMessage(quote);
|
||||||
|
}
|
||||||
|
|
||||||
String filename = draft.getFile();
|
String filename = draft.getFile();
|
||||||
if (filename.isEmpty() || !new File(filename).exists()) {
|
if (filename.isEmpty() || !new File(filename).exists()) {
|
||||||
future.set(!text.isEmpty());
|
future.set(!text.isEmpty());
|
||||||
|
@ -1047,12 +1055,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
final SettableFuture<Integer> future = new SettableFuture<>();
|
final SettableFuture<Integer> future = new SettableFuture<>();
|
||||||
|
|
||||||
DcMsg msg = null;
|
DcMsg msg = null;
|
||||||
|
Optional<QuoteModel> quote = inputPanel.getQuote();
|
||||||
Integer recompress = 0;
|
Integer recompress = 0;
|
||||||
|
|
||||||
// for a quick ui feedback, we clear the related controls immediately on sending messages.
|
// 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.
|
// for drafts, however, we do not change the controls, the activity may be resumed.
|
||||||
if (action==ACTION_SEND_OUT) {
|
if (action==ACTION_SEND_OUT) {
|
||||||
composeText.setText("");
|
composeText.setText("");
|
||||||
|
inputPanel.clearQuote();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(slideDeck!=null) {
|
if(slideDeck!=null) {
|
||||||
|
@ -1095,6 +1105,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
msg.setText(body);
|
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
|
// msg may still be null to clear drafts
|
||||||
new AsyncTask<Object, Void, Void>() {
|
new AsyncTask<Object, Void, Void>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -1440,7 +1455,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
@Override
|
||||||
|
|
|
@ -21,6 +21,8 @@ import androidx.annotation.LayoutRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -49,6 +51,8 @@ import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.thoughtcrime.securesms.ConversationItem.PULSE_HIGHLIGHT_MILLIS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DC adapter for a conversation thread. Ultimately
|
* A DC adapter for a conversation thread. Ultimately
|
||||||
* used by ConversationActivity to display a conversation
|
* used by ConversationActivity to display a conversation
|
||||||
|
@ -92,6 +96,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||||
private @NonNull DcChat dcChat;
|
private @NonNull DcChat dcChat;
|
||||||
private @NonNull int[] dcMsgList = new int[0];
|
private @NonNull int[] dcMsgList = new int[0];
|
||||||
private int positionToPulseHighlight = -1;
|
private int positionToPulseHighlight = -1;
|
||||||
|
private int positionCurrentlyPulseHighlighting = -1;
|
||||||
|
private long pulseHighlightingSince = -1;
|
||||||
private int lastSeenPosition = -1;
|
private int lastSeenPosition = -1;
|
||||||
private long lastSeen = -1;
|
private long lastSeen = -1;
|
||||||
|
|
||||||
|
@ -225,12 +231,24 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
ConversationAdapter.ViewHolder holder = (ConversationAdapter.ViewHolder)viewHolder;
|
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);
|
holder.getItem().bind(getMsg(position), dcChat, glideRequests, locale, batchSelected, recipient, pulseHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
if (pulseHighlight) {
|
@Override
|
||||||
positionToPulseHighlight = -1;
|
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.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
||||||
import org.thoughtcrime.securesms.videochat.VideochatUtil;
|
import org.thoughtcrime.securesms.videochat.VideochatUtil;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -106,6 +107,7 @@ public class ConversationFragment extends Fragment
|
||||||
private Recipient recipient;
|
private Recipient recipient;
|
||||||
private long chatId;
|
private long chatId;
|
||||||
private int startingPosition;
|
private int startingPosition;
|
||||||
|
private int startingMsgId;
|
||||||
private boolean firstLoad;
|
private boolean firstLoad;
|
||||||
private ActionMode actionMode;
|
private ActionMode actionMode;
|
||||||
private Locale locale;
|
private Locale locale;
|
||||||
|
@ -160,6 +162,12 @@ public class ConversationFragment extends Fragment
|
||||||
list.setLayoutManager(layoutManager);
|
list.setLayoutManager(layoutManager);
|
||||||
list.setItemAnimator(null);
|
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)
|
// 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"
|
// 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);
|
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.chatId = this.getActivity().getIntent().getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
|
||||||
this.recipient = Recipient.from(getActivity(), Address.fromChat((int)this.chatId));
|
this.recipient = Recipient.from(getActivity(), Address.fromChat((int)this.chatId));
|
||||||
this.startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
|
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;
|
this.firstLoad = true;
|
||||||
|
|
||||||
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
|
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
|
||||||
|
@ -311,10 +320,16 @@ public class ConversationFragment extends Fragment
|
||||||
if (messageRecords.size() > 1) {
|
if (messageRecords.size() > 1) {
|
||||||
menu.findItem(R.id.menu_context_details).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_save_attachment).setVisible(false);
|
||||||
|
menu.findItem(R.id.menu_context_reply).setVisible(false);
|
||||||
|
menu.findItem(R.id.menu_context_reply_privately).setVisible(false);
|
||||||
} else {
|
} else {
|
||||||
DcMsg messageRecord = messageRecords.iterator().next();
|
DcMsg messageRecord = messageRecords.iterator().next();
|
||||||
|
DcChat chat = getListAdapter().getChat();
|
||||||
menu.findItem(R.id.menu_context_details).setVisible(true);
|
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_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) {
|
private void handleResendMessage(final DcMsg message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
private void handleReplyMessage(final DcMsg message) {
|
private void handleReplyMessage(final DcMsg message) {
|
||||||
|
if (getActivity() != null) {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
|
||||||
|
}
|
||||||
|
|
||||||
listener.handleReplyMessage(message);
|
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) {
|
private void handleSaveAttachment(final DcMsg message) {
|
||||||
SaveAttachmentTask.showWarningDialog(getContext(), (dialogInterface, i) -> {
|
SaveAttachmentTask.showWarningDialog(getContext(), (dialogInterface, i) -> {
|
||||||
Permissions.with(getActivity())
|
Permissions.with(getActivity())
|
||||||
|
@ -492,13 +530,19 @@ public class ConversationFragment extends Fragment
|
||||||
pixelOffset = (firstView == null) ? 0 : list.getBottom() - firstView.getBottom() - list.getPaddingBottom();
|
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);
|
int[] msgs = DcHelper.getContext(getContext()).getChatMsgs((int) chatId, 0, 0);
|
||||||
adapter.changeData(msgs);
|
adapter.changeData(msgs);
|
||||||
int lastSeenPosition = adapter.getLastSeenPosition();
|
int lastSeenPosition = adapter.getLastSeenPosition();
|
||||||
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
if (startingPosition >= 0) {
|
if (startingPosition >= 0) {
|
||||||
scrollToStartingPosition(startingPosition);
|
scrollAndHighlight(startingPosition, false);
|
||||||
|
} else if (startingMsgId >= 0) {
|
||||||
|
scrollToMsgId(startingMsgId);
|
||||||
} else {
|
} else {
|
||||||
scrollToLastSeenPosition(lastSeenPosition);
|
scrollToLastSeenPosition(lastSeenPosition);
|
||||||
}
|
}
|
||||||
|
@ -529,10 +573,14 @@ public class ConversationFragment extends Fragment
|
||||||
floatingLocationButton.setVisibility(dcContext.isSendingLocationsToChat((int) chatId)? View.VISIBLE : View.GONE);
|
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.post(() -> {
|
||||||
list.getLayoutManager().scrollToPosition(startingPosition);
|
if (smooth) {
|
||||||
getListAdapter().pulseHighlightItem(startingPosition);
|
list.smoothScrollToPosition(pos);
|
||||||
|
} else {
|
||||||
|
list.scrollToPosition(pos);
|
||||||
|
}
|
||||||
|
getListAdapter().pulseHighlightItem(pos);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,7 +594,9 @@ public class ConversationFragment extends Fragment
|
||||||
ConversationAdapter adapter = (ConversationAdapter)list.getAdapter();
|
ConversationAdapter adapter = (ConversationAdapter)list.getAdapter();
|
||||||
int position = adapter.msgIdToPosition(msgId);
|
int position = adapter.msgIdToPosition(msgId);
|
||||||
if (position!=-1) {
|
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) {
|
if (getListAdapter().getSelectedItems().size() == 0) {
|
||||||
actionMode.finish();
|
actionMode.finish();
|
||||||
} else {
|
} 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()));
|
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -768,6 +820,48 @@ public class ConversationFragment extends Fragment
|
||||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
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
|
@Override
|
||||||
|
@ -799,6 +893,7 @@ public class ConversationFragment extends Fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
setCorrectMenuVisibility(menu);
|
setCorrectMenuVisibility(menu);
|
||||||
|
AdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -848,6 +943,9 @@ public class ConversationFragment extends Fragment
|
||||||
handleReplyMessage(getSelectedMessageRecord());
|
handleReplyMessage(getSelectedMessageRecord());
|
||||||
actionMode.finish();
|
actionMode.finish();
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.menu_context_reply_privately:
|
||||||
|
handleReplyMessagePrivately(getSelectedMessageRecord());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -25,12 +25,15 @@ import androidx.annotation.DimenRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.ConversationItemFooter;
|
||||||
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
||||||
import org.thoughtcrime.securesms.components.DocumentView;
|
import org.thoughtcrime.securesms.components.DocumentView;
|
||||||
|
import org.thoughtcrime.securesms.components.QuoteView;
|
||||||
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
|
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
|
||||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
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 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 Pattern CMD_PATTERN = Pattern.compile("(?<=^|\\s)/[a-zA-Z][a-zA-Z@\\d_/.-]{0,254}");
|
||||||
private static final int MAX_MEASURE_CALLS = 3;
|
private static final int MAX_MEASURE_CALLS = 3;
|
||||||
|
static long PULSE_HIGHLIGHT_MILLIS = 500;
|
||||||
|
|
||||||
private DcMsg messageRecord;
|
private DcMsg messageRecord;
|
||||||
private DcChat dcChat;
|
private DcChat dcChat;
|
||||||
|
@ -92,12 +101,14 @@ public class ConversationItem extends LinearLayout
|
||||||
private GlideRequests glideRequests;
|
private GlideRequests glideRequests;
|
||||||
|
|
||||||
protected ViewGroup bodyBubble;
|
protected ViewGroup bodyBubble;
|
||||||
|
protected View reply;
|
||||||
|
@Nullable private QuoteView quoteView;
|
||||||
private TextView bodyText;
|
private TextView bodyText;
|
||||||
private ConversationItemFooter footer;
|
private ConversationItemFooter footer;
|
||||||
private TextView groupSender;
|
private TextView groupSender;
|
||||||
private View groupSenderHolder;
|
private View groupSenderHolder;
|
||||||
private AvatarImageView contactPhoto;
|
private AvatarImageView contactPhoto;
|
||||||
private ViewGroup contactPhotoHolder;
|
protected ViewGroup contactPhotoHolder;
|
||||||
private ViewGroup container;
|
private ViewGroup container;
|
||||||
|
|
||||||
private @NonNull Set<DcMsg> batchSelected = new HashSet<>();
|
private @NonNull Set<DcMsg> batchSelected = new HashSet<>();
|
||||||
|
@ -107,6 +118,8 @@ public class ConversationItem extends LinearLayout
|
||||||
private @NonNull Stub<DocumentView> documentViewStub;
|
private @NonNull Stub<DocumentView> documentViewStub;
|
||||||
private @Nullable EventListener eventListener;
|
private @Nullable EventListener eventListener;
|
||||||
|
|
||||||
|
private int measureCalls;
|
||||||
|
|
||||||
private int incomingBubbleColor;
|
private int incomingBubbleColor;
|
||||||
private int outgoingBubbleColor;
|
private int outgoingBubbleColor;
|
||||||
private int forwardedTitleColor;
|
private int forwardedTitleColor;
|
||||||
|
@ -147,7 +160,9 @@ public class ConversationItem extends LinearLayout
|
||||||
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
|
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
|
||||||
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
|
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
|
||||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||||
|
this.quoteView = findViewById(R.id.quote_view);
|
||||||
this.container = findViewById(R.id.container);
|
this.container = findViewById(R.id.container);
|
||||||
|
this.reply = findViewById(R.id.reply_icon);
|
||||||
|
|
||||||
setOnClickListener(new ClickListener(null));
|
setOnClickListener(new ClickListener(null));
|
||||||
|
|
||||||
|
@ -189,6 +204,8 @@ public class ConversationItem extends LinearLayout
|
||||||
setAuthor(messageRecord, groupThread);
|
setAuthor(messageRecord, groupThread);
|
||||||
setMessageSpacing(context);
|
setMessageSpacing(context);
|
||||||
setFooter(messageRecord, locale);
|
setFooter(messageRecord, locale);
|
||||||
|
setQuote(messageRecord);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -197,6 +214,14 @@ public class ConversationItem extends LinearLayout
|
||||||
this.eventListener = eventListener;
|
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
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
@ -204,6 +229,32 @@ public class ConversationItem extends LinearLayout
|
||||||
if (isInEditMode()) {
|
if (isInEditMode()) {
|
||||||
return;
|
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() {
|
private void initializeAttributes() {
|
||||||
|
@ -245,7 +296,7 @@ public class ConversationItem extends LinearLayout
|
||||||
} else if (pulseHighlight) {
|
} else if (pulseHighlight) {
|
||||||
setBackgroundResource(R.drawable.conversation_item_background_animated);
|
setBackgroundResource(R.drawable.conversation_item_background_animated);
|
||||||
setSelected(true);
|
setSelected(true);
|
||||||
postDelayed(() -> setSelected(false), 1500);
|
postDelayed(() -> setSelected(false), PULSE_HIGHLIGHT_MILLIS);
|
||||||
} else {
|
} else {
|
||||||
setSelected(false);
|
setSelected(false);
|
||||||
}
|
}
|
||||||
|
@ -273,6 +324,10 @@ public class ConversationItem extends LinearLayout
|
||||||
return type==DcMsg.DC_MSG_AUDIO || type==DcMsg.DC_MSG_VOICE;
|
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) {
|
private boolean hasThumbnail(DcMsg messageRecord) {
|
||||||
int type = messageRecord.getType();
|
int type = messageRecord.getType();
|
||||||
return type==DcMsg.DC_MSG_GIF || type==DcMsg.DC_MSG_IMAGE || type==DcMsg.DC_MSG_VIDEO;
|
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;
|
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) {
|
private void setGutterSizes(@NonNull DcMsg current, boolean isGroupThread) {
|
||||||
if (isGroupThread && current.isOutgoing()) {
|
if (isGroupThread && current.isOutgoing()) {
|
||||||
ViewUtil.setLeftMargin(container, readDimen(R.dimen.conversation_group_left_gutter));
|
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) {
|
else if (groupThread && !messageRecord.isOutgoing() && dcContact !=null) {
|
||||||
this.groupSender.setText(dcContact.getDisplayName());
|
this.groupSender.setText(dcContact.getDisplayName());
|
||||||
|
|
||||||
int rgb = dcContact.getColor();
|
this.groupSender.setTextColor(dcContact.getArgbColor());
|
||||||
int argb = Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb));
|
|
||||||
this.groupSender.setTextColor(argb);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -589,6 +690,21 @@ public class ConversationItem extends LinearLayout
|
||||||
return context.getResources().getDimensionPixelOffset(dimenId);
|
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
|
/// Event handlers
|
||||||
|
|
||||||
private void handleDeadDropClick() {
|
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);
|
ApplicationDcContext dcContext = DcHelper.getContext(this);
|
||||||
|
|
||||||
// add welcome message
|
// add welcome message
|
||||||
dcContext.updateDeviceChats();
|
// dcContext.updateDeviceChats();
|
||||||
|
|
||||||
// update messages - for new messages, do not reuse or modify strings but create new ones.
|
// 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.
|
// 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.Context;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.annotation.NonNull;
|
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() {
|
private double getProgress() {
|
||||||
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
|
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -9,6 +11,7 @@ import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.view.animation.AlphaAnimation;
|
import android.view.animation.AlphaAnimation;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationSet;
|
import android.view.animation.AnimationSet;
|
||||||
|
@ -19,23 +22,32 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
|
|
||||||
|
import com.b44t.messenger.DcMsg;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
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.Prefs;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||||
|
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class InputPanel extends LinearLayout
|
public class InputPanel extends ConstraintLayout
|
||||||
implements MicrophoneRecorderView.Listener,
|
implements MicrophoneRecorderView.Listener,
|
||||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||||
EmojiKeyboardProvider.EmojiEventListener
|
EmojiKeyboardProvider.EmojiEventListener
|
||||||
|
@ -45,6 +57,7 @@ public class InputPanel extends LinearLayout
|
||||||
|
|
||||||
private static final int FADE_TIME = 150;
|
private static final int FADE_TIME = 150;
|
||||||
|
|
||||||
|
private QuoteView quoteView;
|
||||||
private EmojiToggle mediaKeyboard;
|
private EmojiToggle mediaKeyboard;
|
||||||
private ComposeText composeText;
|
private ComposeText composeText;
|
||||||
private View quickCameraToggle;
|
private View quickCameraToggle;
|
||||||
|
@ -55,6 +68,7 @@ public class InputPanel extends LinearLayout
|
||||||
private MicrophoneRecorderView microphoneRecorderView;
|
private MicrophoneRecorderView microphoneRecorderView;
|
||||||
private SlideToCancel slideToCancel;
|
private SlideToCancel slideToCancel;
|
||||||
private RecordTime recordTime;
|
private RecordTime recordTime;
|
||||||
|
private ValueAnimator quoteAnimator;
|
||||||
|
|
||||||
private @Nullable Listener listener;
|
private @Nullable Listener listener;
|
||||||
private boolean emojiVisible;
|
private boolean emojiVisible;
|
||||||
|
@ -76,7 +90,9 @@ public class InputPanel extends LinearLayout
|
||||||
public void onFinishInflate() {
|
public void onFinishInflate() {
|
||||||
super.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.mediaKeyboard = findViewById(R.id.emoji_toggle);
|
||||||
this.composeText = findViewById(R.id.embedded_text_editor);
|
this.composeText = findViewById(R.id.embedded_text_editor);
|
||||||
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
||||||
|
@ -101,6 +117,8 @@ public class InputPanel extends LinearLayout
|
||||||
mediaKeyboard.setVisibility(View.VISIBLE);
|
mediaKeyboard.setVisibility(View.VISIBLE);
|
||||||
emojiVisible = true;
|
emojiVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quoteDismiss.setOnClickListener(v -> clearQuote());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setListener(final @NonNull Listener listener) {
|
public void setListener(final @NonNull Listener listener) {
|
||||||
|
@ -113,6 +131,81 @@ public class InputPanel extends LinearLayout
|
||||||
composeText.setMediaListener(listener);
|
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) {
|
public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) {
|
||||||
this.mediaKeyboard.attach(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);
|
large = storedSizes.get(landscapeWidth + "x" + landscapeHeight);
|
||||||
if (large == null) return; // no baseline. can't work.
|
if (large == null) return; // no baseline. can't work.
|
||||||
Bitmap original = ((BitmapDrawable) large).getBitmap();
|
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);
|
Bitmap cropped = Bitmap.createBitmap(original, 0, 0, width, height);
|
||||||
Drawable croppedDrawable = new BitmapDrawable(getResources(), cropped);
|
Drawable croppedDrawable = new BitmapDrawable(getResources(), cropped);
|
||||||
overrideDrawable(croppedDrawable);
|
overrideDrawable(croppedDrawable);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.SearchView;
|
import androidx.appcompat.widget.SearchView;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewAnimationUtils;
|
import android.view.ViewAnimationUtils;
|
||||||
|
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||||
|
|
||||||
public class SearchToolbar extends LinearLayout {
|
public class SearchToolbar extends LinearLayout {
|
||||||
|
|
||||||
|
private static final String TAG = SearchToolbar.class.getSimpleName();
|
||||||
private float x, y;
|
private float x, y;
|
||||||
private MenuItem searchItem;
|
private MenuItem searchItem;
|
||||||
private SearchListener listener;
|
private SearchListener listener;
|
||||||
|
@ -47,6 +49,10 @@ public class SearchToolbar extends LinearLayout {
|
||||||
setOrientation(VERTICAL);
|
setOrientation(VERTICAL);
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
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 drawable = getContext().getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp);
|
||||||
drawable.mutate();
|
drawable.mutate();
|
||||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.connect;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.b44t.messenger.DcContext;
|
import com.b44t.messenger.DcContext;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
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_MEDIA_QUALITY = "media_quality";
|
||||||
public static final String CONFIG_WEBRTC_INSTANCE = "webrtc_instance";
|
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;
|
return ApplicationContext.getInstance(context).dcContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class AudioSlide extends Slide {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasImage() {
|
public boolean hasImage() {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
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() {
|
public @Nullable DocumentSlide getDocumentSlide() {
|
||||||
for (Slide slide: slides) {
|
for (Slide slide: slides) {
|
||||||
if (slide.hasDocument()) {
|
if (slide.hasDocument()) {
|
||||||
|
|
|
@ -137,6 +137,10 @@ public class Recipient {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable DcContact getDcContact() {
|
||||||
|
return dcContact;
|
||||||
|
}
|
||||||
|
|
||||||
public @NonNull Address getAddress() {
|
public @NonNull Address getAddress() {
|
||||||
return address;
|
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.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
|
@ -240,4 +241,8 @@ public class ViewUtil {
|
||||||
public static void setPaddingBottom(@NonNull View view, int padding) {
|
public static void setPaddingBottom(@NonNull View view, int padding) {
|
||||||
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), 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