Add internal pre-alpha support for receiving reactions.
24
protobuf/Database.proto
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package signal;
|
||||
|
||||
option java_package = "org.thoughtcrime.securesms.database.model";
|
||||
option java_outer_classname = "DatabaseProtos";
|
||||
|
||||
|
||||
message ReactionList {
|
||||
message Reaction {
|
||||
string emoji = 1;
|
||||
uint64 author = 2;
|
||||
uint64 sentTime = 3;
|
||||
uint64 receivedTime = 4;
|
||||
}
|
||||
|
||||
repeated Reaction reactions = 1;
|
||||
}
|
16
res/animator/reactions_scrubber_hide.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<objectAnimator
|
||||
android:duration="@integer/reaction_scrubber_reveal_emoji_duration"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="translationY"
|
||||
android:valueTo="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
android:valueFrom="@dimen/reaction_scrubber_anim_end_translation_y" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="@integer/reaction_scrubber_reveal_emoji_duration"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="alpha"
|
||||
android:valueTo="0"
|
||||
android:valueFrom="1" />
|
||||
</set>
|
16
res/animator/reactions_scrubber_reveal.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<objectAnimator
|
||||
android:duration="@integer/reaction_scrubber_reveal_duration"
|
||||
android:interpolator="@android:anim/linear_interpolator"
|
||||
android:propertyName="translationY"
|
||||
android:valueFrom="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
android:valueTo="@dimen/reaction_scrubber_anim_end_translation_y" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="@integer/reaction_scrubber_reveal_duration"
|
||||
android:interpolator="@android:anim/linear_interpolator"
|
||||
android:propertyName="alpha"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1" />
|
||||
</set>
|
Before Width: | Height: | Size: 470 B |
Before Width: | Height: | Size: 96 B |
Before Width: | Height: | Size: 208 B |
Before Width: | Height: | Size: 366 B |
Before Width: | Height: | Size: 82 B |
Before Width: | Height: | Size: 158 B |
Before Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 110 B |
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 756 B |
Before Width: | Height: | Size: 132 B |
Before Width: | Height: | Size: 382 B |
Before Width: | Height: | Size: 892 B |
Before Width: | Height: | Size: 480 B |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/transparent_black_80" />
|
||||
<corners android:radius="30dp" />
|
||||
</shape>
|
5
res/drawable/ic_copy_outline_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M15.95,19.5A3,3 0,0 1,13 22H6a3,3 0,0 1,-3 -3V9A3,3 0,0 1,6 6h0.5V7.5H6A1.5,1.5 0,0 0,4.5 9V19A1.5,1.5 0,0 0,6 20.5h7a1.5,1.5 0,0 0,1.408 -1ZM18,3.5H11A1.5,1.5 0,0 0,9.5 5V15A1.5,1.5 0,0 0,11 16.5h7A1.5,1.5 0,0 0,19.5 15V5A1.5,1.5 0,0 0,18 3.5M18,2a3,3 0,0 1,3 3V15a3,3 0,0 1,-3 3H11a3,3 0,0 1,-3 -3V5a3,3 0,0 1,3 -3Z"/>
|
||||
</vector>
|
5
res/drawable/ic_copy_solid_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M21,5V15a3,3 0,0 1,-3 3H11a3,3 0,0 1,-3 -3V5a3,3 0,0 1,3 -3h7A3,3 0,0 1,21 5ZM11,19.5A4.505,4.505 0,0 1,6.5 15V6H6A3,3 0,0 0,3 9V19a3,3 0,0 0,3 3h7a3,3 0,0 0,2.95 -2.5Z"/>
|
||||
</vector>
|
@ -4,6 +4,6 @@
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12.91,3.91h0m1.71,2.16V9.32l-1.31,0.17A11,11 0,0 0,3.84 17c-0.12,0.33 -0.8,1.59 -0.8,1.59s1,-1 1.3,-1.22A17.36,17.36 0,0 1,13 14.5l1.62,-0.12v3.48l-0.06,0.81L15,18 21,12 14.9,5.9l-0.34,-0.54ZM13.48,2.58a0.76,0.76 0,0 1,0.49 0.27l8.79,8.8a0.48,0.48 0,0 1,0 0.7L14,21.15a0.76,0.76 0,0 1,-0.49 0.27c-0.22,0 -0.36,-0.22 -0.36,-0.63V16c-5,0.39 -8.83,2.48 -11.37,6 -0.14,0.2 -0.27,0.29 -0.37,0.29s-0.2,-0.17 -0.16,-0.5C2,14.43 5.59,9 13.12,8V3.21c0,-0.41 0.14,-0.63 0.36,-0.63Z"/>
|
||||
</vector>
|
5
res/drawable/ic_forward_solid_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M2.116,21.426A14.706,14.706 0,0 1,13.021 16v4.793c0,0.664 0.384,0.823 0.853,0.353l8.793,-8.792a0.5,0.5 0,0 0,0 -0.708L13.874,2.854c-0.469,-0.47 -0.853,-0.311 -0.853,0.353V8C5.757,8.934 2.2,14.051 1.217,21.059 1.071,22.1 1.463,22.249 2.116,21.426Z"/>
|
||||
</vector>
|
5
res/drawable/ic_info_solid_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M12,1A11,11 0,1 0,23 12,11 11,0 0,0 12,1ZM10.94,5.939A1.5,1.5 0,0 1,13.5 7a1.5,1.5 0,0 1,-2.56 1.06,1.5 1.5,0 0,1 0,-2.121ZM15,18H9V16.5h2.5v-5h-2V10H13v6.5h2Z"/>
|
||||
</vector>
|
5
res/drawable/ic_reply_outline_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M9.5,5.621v3.7l1.309,0.169a10.932,10.932 0,0 1,9.364 7.253c0.161,0.406 0.8,1.756 0.8,1.756A19.408,19.408 0,0 0,19.5 17.2,17.455 17.455,0 0,0 11.115,14.5L9.5,14.38v4L3.121,12 9.5,5.621m1.137,-3.037a0.758,0.758 0,0 0,-0.491 0.27L1.354,11.646a0.5,0.5 0,0 0,0 0.708l8.792,8.792a0.758,0.758 0,0 0,0.491 0.27c0.219,0 0.363,-0.217 0.363,-0.623V16a14.706,14.706 0,0 1,10.905 5.426c0.282,0.355 0.514,0.529 0.677,0.529 0.214,0 0.3,-0.3 0.222,-0.9C21.822,14.051 18.264,8.934 11,8V3.207c0,-0.406 -0.144,-0.623 -0.363,-0.623Z"/>
|
||||
</vector>
|
5
res/drawable/ic_reply_solid_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M21.905,21.426A14.706,14.706 0,0 0,11 16v4.793c0,0.664 -0.384,0.823 -0.854,0.353L1.354,12.354a0.5,0.5 0,0 1,0 -0.708l8.792,-8.792c0.47,-0.47 0.854,-0.311 0.854,0.353V8c7.264,0.934 10.822,6.051 11.8,13.059C22.95,22.1 22.558,22.249 21.905,21.426Z"/>
|
||||
</vector>
|
5
res/drawable/ic_save_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M22,10v9a3,3 0,0 1,-3 3L5,22a3,3 0,0 1,-3 -3L2,10A3,3 0,0 1,5 7L9.75,7L9.75,8.5L5,8.5A1.5,1.5 0,0 0,3.5 10v9A1.5,1.5 0,0 0,5 20.5L19,20.5A1.5,1.5 0,0 0,20.5 19L20.5,10A1.5,1.5 0,0 0,19 8.5L14.25,8.5L14.25,7L19,7A3,3 0,0 1,22 10ZM15.419,11.47L13.586,13.3l-0.82,1.148L12.766,2h-1.5L11.266,14.45l-0.742,-1.039L8.581,11.485 7.525,12.55 12.012,17l4.467,-4.468Z"/>
|
||||
</vector>
|
5
res/drawable/ic_select_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M8.5,4.75L22,4.75v1.5L8.5,6.25ZM8.5,12.75L22,12.75v-1.5L8.5,11.25ZM8.5,19.25L22,19.25v-1.5L8.5,17.75ZM4.5,4.5a1,1 0,1 0,1 1,1 1,0 0,0 -1,-1M4.5,3A2.5,2.5 0,1 1,2 5.5,2.5 2.5,0 0,1 4.5,3ZM4.5,11a1,1 0,1 0,1 1,1 1,0 0,0 -1,-1m0,-1.5A2.5,2.5 0,1 1,2 12,2.5 2.5,0 0,1 4.5,9.5ZM4.5,17.5a1,1 0,1 0,1 1,1 1,0 0,0 -1,-1m0,-1.5A2.5,2.5 0,1 1,2 18.5,2.5 2.5,0 0,1 4.5,16Z"/>
|
||||
</vector>
|
5
res/drawable/ic_trash_outline_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M12.75,18h-1.5L11.25,8h1.5ZM16.5,8L15,8L15,18h1.5ZM9,8L7.5,8L7.5,18L9,18ZM22,5.5L20,5.5L20,19a3,3 0,0 1,-3 3L7,22a3,3 0,0 1,-3 -3L4,5.5L2,5.5L2,4L7.571,4l1.05,-1.837h0A1.5,1.5 0,0 1,9.866 1.5h4.269a1.5,1.5 0,0 1,1.235 0.651L16.43,4L22,4ZM9.3,4h5.4l-0.573,-1L9.871,3ZM18.5,5.5L5.5,5.5L5.5,19A1.5,1.5 0,0 0,7 20.5L17,20.5A1.5,1.5 0,0 0,18.5 19Z"/>
|
||||
</vector>
|
5
res/drawable/ic_trash_solid_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M16.43,4 L15.37,2.151A1.5,1.5 0,0 0,14.135 1.5L9.866,1.5a1.5,1.5 0,0 0,-1.244 0.663h0L7.571,4L2,4L2,5.5L4,5.5L4,19a3,3 0,0 0,3 3L17,22a3,3 0,0 0,3 -3L20,5.5h2L22,4ZM9,18L7.5,18L7.5,8L9,8ZM12.75,18h-1.5L11.25,8h1.5ZM9.3,4l0.572,-1h4.257L14.7,4ZM16.5,18L15,18L15,8h1.5Z"/>
|
||||
</vector>
|
9
res/drawable/ic_x_conversation.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?conversation_subtitle_color"
|
||||
android:pathData="M20.5,4.5l-1,-1l-7.5,7.4l-7.5,-7.4l-1,1l7.4,7.5l-7.4,7.5l1,1l7.5,-7.4l7.5,7.4l1,-1l-7.4,-7.5z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="18dp" />
|
||||
<solid android:color="@color/core_grey_60" />
|
||||
</shape>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="18dp" />
|
||||
<solid android:color="@color/core_grey_05" />
|
||||
</shape>
|
5
res/drawable/reactions_old_background_dark.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="@color/transparent_white_30" />
|
||||
</shape>
|
8
res/drawable/reactions_recv_background_dark.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/core_grey_75" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/core_grey_95" />
|
||||
</shape>
|
8
res/drawable/reactions_recv_background_light.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/core_grey_05" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/white" />
|
||||
</shape>
|
8
res/drawable/reactions_send_background_dark.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/core_grey_45" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/core_grey_95" />
|
||||
</shape>
|
8
res/drawable/reactions_send_background_light.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/core_grey_25" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/white" />
|
||||
</shape>
|
@ -142,4 +142,5 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<include layout="@layout/conversation_reaction_scrubber" />
|
||||
</FrameLayout>
|
||||
|
@ -55,5 +55,4 @@
|
||||
android:alpha="0.9"
|
||||
android:contentDescription="@string/conversation_fragment__scroll_to_the_bottom_content_description"
|
||||
android:src="@drawable/ic_scroll_down"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
@ -27,7 +27,7 @@
|
||||
android:id="@+id/reply_icon"
|
||||
android:layout_width="@dimen/conversation_item_reply_size"
|
||||
android:layout_height="@dimen/conversation_item_reply_size"
|
||||
android:src="@drawable/ic_reply_white_24dp"
|
||||
app:srcCompat="?menu_reply_icon"
|
||||
android:tint="?compose_icon_tint"
|
||||
android:alpha="0"
|
||||
android:layout_alignTop="@id/body_bubble"
|
||||
@ -231,5 +231,41 @@
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.MaxHeightFrameLayout
|
||||
android:id="@+id/reactions_bubbles_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dip"
|
||||
android:layout_alignTop="@id/body_bubble"
|
||||
android:layout_alignBottom="@id/body_bubble"
|
||||
android:layout_marginStart="@dimen/reactions_bubble_margin"
|
||||
android:layout_toEndOf="@id/body_bubble"
|
||||
android:visibility="gone"
|
||||
app:mhfl_maxHeight="@dimen/reactions_bubble_container_max_height"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reactions_bubbles_secondary"
|
||||
android:layout_width="@dimen/reactions_bubble_size"
|
||||
android:layout_height="@dimen/reactions_bubble_size"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/reactions_recv_background"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="@dimen/reactions_bubble_text_size"
|
||||
tools:ignore="SpUsage" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reactions_bubbles_primary"
|
||||
android:layout_width="@dimen/reactions_bubble_size"
|
||||
android:layout_height="@dimen/reactions_bubble_size"
|
||||
android:layout_gravity="top"
|
||||
android:background="?attr/reactions_recv_background"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="@dimen/reactions_bubble_text_size"
|
||||
tools:ignore="SpUsage" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.MaxHeightFrameLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</org.thoughtcrime.securesms.conversation.ConversationItem>
|
||||
|
@ -1,71 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<org.thoughtcrime.securesms.conversation.ConversationItem xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/conversation_item"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/conversation_item_background"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/container"
|
||||
android:nextFocusRight="@id/embedded_text_editor"
|
||||
android:background="@drawable/conversation_item_background"
|
||||
android:clipToPadding="false"
|
||||
android:clipChildren="false">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/conversation_individual_right_gutter"
|
||||
android:clipToPadding="false"
|
||||
android:clipChildren="false">
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reply_icon"
|
||||
android:layout_width="@dimen/conversation_item_reply_size"
|
||||
android:layout_height="@dimen/conversation_item_reply_size"
|
||||
android:alpha="0"
|
||||
android:src="@drawable/ic_reply_white_24dp"
|
||||
android:tint="?compose_icon_tint"
|
||||
android:layout_alignStart="@id/body_bubble"
|
||||
android:layout_alignTop="@id/body_bubble"
|
||||
android:layout_alignBottom="@id/body_bubble"
|
||||
android:layout_alignStart="@id/body_bubble" />
|
||||
android:alpha="0"
|
||||
app:srcCompat="?menu_reply_icon"
|
||||
android:tint="?compose_icon_tint" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.ConversationItemBodyBubble
|
||||
android:id="@+id/body_bubble"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_toStartOf="@+id/indicators_parent"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_marginStart="@dimen/message_bubble_edge_margin"
|
||||
android:clipToPadding="false"
|
||||
android:clipChildren="false"
|
||||
android:layout_toStartOf="@+id/indicators_parent"
|
||||
android:background="@color/white"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
tools:backgroundTint="@color/core_grey_05">
|
||||
|
||||
<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_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:visibility="gone"
|
||||
app:message_type="outgoing"
|
||||
app:quote_colorPrimary="?attr/conversation_item_quote_text_color"
|
||||
app:quote_colorSecondary="?attr/conversation_item_quote_text_color"
|
||||
tools:visibility="visible"/>
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/shared_contact_view_stub"
|
||||
android:layout="@layout/conversation_item_sent_shared_contact"
|
||||
android:layout_width="@dimen/media_bubble_default_dimens"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:visibility="gone"/>
|
||||
android:layout="@layout/conversation_item_sent_shared_contact"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/image_view_stub"
|
||||
@ -87,55 +86,55 @@
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/audio_view_stub"
|
||||
android:layout="@layout/conversation_item_sent_audio"
|
||||
android:layout_width="210dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
android:layout="@layout/conversation_item_sent_audio" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/document_view_stub"
|
||||
android:layout="@layout/conversation_item_sent_document"
|
||||
android:layout_width="210dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
android:layout="@layout/conversation_item_sent_document" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/revealable_view_stub"
|
||||
android:layout="@layout/conversation_item_sent_revealable"
|
||||
android:layout_width="148dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
android:layout="@layout/conversation_item_sent_revealable" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/conversation_item_body"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:ellipsize="end"
|
||||
android:textColor="?conversation_item_sent_text_primary_color"
|
||||
android:textColorLink="?conversation_item_sent_text_primary_color"
|
||||
android:ellipsize="end"
|
||||
app:scaleEmojis="true"
|
||||
app:emoji_maxLength="1000"
|
||||
tools:text="Mango pickle lorem ipsum"/>
|
||||
app:scaleEmojis="true"
|
||||
tools:text="Mango pickle lorem ipsum" />
|
||||
|
||||
<View
|
||||
android:id="@+id/group_sender_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_message_sender"
|
||||
@ -153,29 +152,29 @@
|
||||
android:id="@+id/conversation_item_footer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
|
||||
android:gravity="end"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:footer_text_color="?attr/conversation_item_sent_text_secondary_color"
|
||||
app:footer_icon_color="?attr/conversation_item_sent_icon_color"/>
|
||||
android:gravity="end"
|
||||
app:footer_icon_color="?attr/conversation_item_sent_icon_color"
|
||||
app:footer_text_color="?attr/conversation_item_sent_text_secondary_color" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ConversationItemFooter
|
||||
android:id="@+id/conversation_item_sticker_footer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
|
||||
android:gravity="end"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="end"
|
||||
android:visibility="gone"
|
||||
app:footer_text_color="?conversation_sticker_footer_text_color"
|
||||
app:footer_icon_color="?conversation_sticker_footer_icon_color"/>
|
||||
app:footer_icon_color="?conversation_sticker_footer_icon_color"
|
||||
app:footer_text_color="?conversation_sticker_footer_text_color" />
|
||||
|
||||
</org.thoughtcrime.securesms.conversation.ConversationItemBodyBubble>
|
||||
|
||||
@ -183,11 +182,47 @@
|
||||
android:id="@+id/indicators_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="4dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true" />
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.MaxHeightFrameLayout
|
||||
android:id="@+id/reactions_bubbles_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dip"
|
||||
android:layout_alignTop="@id/body_bubble"
|
||||
android:layout_alignBottom="@id/body_bubble"
|
||||
android:layout_marginEnd="@dimen/reactions_bubble_margin"
|
||||
android:layout_toStartOf="@id/body_bubble"
|
||||
android:visibility="gone"
|
||||
app:mhfl_maxHeight="@dimen/reactions_bubble_container_max_height"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reactions_bubbles_secondary"
|
||||
android:layout_width="@dimen/reactions_bubble_size"
|
||||
android:layout_height="@dimen/reactions_bubble_size"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/reactions_recv_background"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="@dimen/reactions_bubble_text_size"
|
||||
tools:ignore="SpUsage" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reactions_bubbles_primary"
|
||||
android:layout_width="@dimen/reactions_bubble_size"
|
||||
android:layout_height="@dimen/reactions_bubble_size"
|
||||
android:layout_gravity="top"
|
||||
android:background="?attr/reactions_recv_background"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="@dimen/reactions_bubble_text_size"
|
||||
tools:ignore="SpUsage" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.MaxHeightFrameLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
11
res/layout/conversation_reaction_long_press_toolbar.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/action_mode_status_bar"
|
||||
android:theme="@style/TextSecure.DarkActionBar.Conversation"
|
||||
app:navigationIcon="@drawable/ic_x_conversation"
|
||||
app:menu="@menu/conversation_reactions_long_press_menu">
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
151
res/layout/conversation_reaction_scrubber.xml
Normal file
@ -0,0 +1,151 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.ConversationReactionOverlay
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/conversation_reaction_scrubber"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="1000dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.MaskView
|
||||
android:id="@+id/conversation_reaction_mask"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
android:background="@color/transparent_black_20" />
|
||||
|
||||
<include
|
||||
android:id="@+id/conversation_reaction_toolbar"
|
||||
layout="@layout/conversation_reaction_long_press_toolbar" />
|
||||
|
||||
<View
|
||||
android:id="@+id/conversation_reaction_scrubber_background"
|
||||
android:layout_width="320dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_marginTop="40dp"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:alpha="0"
|
||||
android:background="?reactions_overlay_scrubber_background"
|
||||
android:elevation="4dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/conversation_reaction_scrubber_foreground"
|
||||
android:layout_width="320dp"
|
||||
android:layout_height="@dimen/conversation_reaction_scrubber_height"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="4dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/conversation_reaction_current_selection_indicator"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:alpha="0"
|
||||
android:background="?attr/reactions_overlay_old_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="@id/reaction_3"
|
||||
app:layout_constraintRight_toRightOf="@id/reaction_3"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reaction_1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/conversation_reaction_picker_emoji_text_size"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reaction_2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/conversation_reaction_picker_emoji_text_size"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_3"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reaction_3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/conversation_reaction_picker_emoji_text_size"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_4"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_2"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reaction_4"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/conversation_reaction_picker_emoji_text_size"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_5"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_3"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reaction_5"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/conversation_reaction_picker_emoji_text_size"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_6"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_4"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reaction_6"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/conversation_reaction_picker_emoji_text_size"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/reaction_7"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_5"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reaction_7"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/conversation_reaction_picker_emoji_text_size"
|
||||
android:translationY="@dimen/reaction_scrubber_anim_start_translation_y"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reaction_6"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.conversation.ConversationReactionOverlay>
|
28
res/layout/reactions_bottom_sheet_dialog_fragment.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:minHeight="340dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reactions_bottom_view_emoji_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/reactions_bottom_sheet_dialog_fragment_emoji_item" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reactions_bottom_view_recipient_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/reactions_bottom_sheet_dialog_fragment_recipient_item" />
|
||||
</LinearLayout>
|
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="62dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/reactions_bottom_view_emoji_item_emoji"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="22dp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="SpUsage"
|
||||
tools:text=":-)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reactions_bottom_view_emoji_item_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="true"
|
||||
android:textColor="?title_text_color_primary"
|
||||
android:textSize="14dp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="SpUsage"
|
||||
tools:text="24" />
|
||||
</LinearLayout>
|
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/reactions_bottom_view_recipient_avatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reactions_bottom_view_recipient_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="?title_text_color_primary"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reactions_bottom_view_recipient_avatar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -88,7 +88,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="?sticker_management_action_button_color"
|
||||
app:srcCompat="@drawable/ic_forward_outline"
|
||||
app:srcCompat="@drawable/ic_forward_outline_24"
|
||||
app:layout_constraintTop_toTopOf="@id/sticker_management_share_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/sticker_management_share_button"
|
||||
app:layout_constraintStart_toStartOf="@id/sticker_management_share_button"
|
||||
|
@ -71,7 +71,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="@color/core_grey_90"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_forward_outline"
|
||||
app:srcCompat="@drawable/ic_forward_outline_24"
|
||||
app:layout_constraintTop_toTopOf="@id/sticker_install_share_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/sticker_install_share_button"
|
||||
app:layout_constraintStart_toStartOf="@id/sticker_install_share_button"
|
||||
|
@ -15,10 +15,11 @@
|
||||
android:icon="?menu_copy_icon"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item android:title="@string/conversation_context__menu_forward_message"
|
||||
android:id="@+id/menu_context_forward"
|
||||
android:icon="?menu_forward_icon"
|
||||
app:showAsAction="always" />
|
||||
<item android:title="@string/conversation_context__menu_reply_to_message"
|
||||
android:id="@+id/menu_context_reply"
|
||||
android:visible="true"
|
||||
android:icon="?menu_reply_icon"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item android:title="@string/conversation_context__menu_resend_message"
|
||||
android:id="@+id/menu_context_resend"
|
||||
@ -31,10 +32,9 @@
|
||||
android:icon="?menu_save_icon"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item android:title="@string/conversation_context__menu_reply_to_message"
|
||||
android:id="@+id/menu_context_reply"
|
||||
android:visible="true"
|
||||
android:icon="?menu_reply_icon"
|
||||
app:showAsAction="always" />
|
||||
<item android:title="@string/conversation_context__menu_forward_message"
|
||||
android:id="@+id/menu_context_forward"
|
||||
android:icon="?menu_forward_icon"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
||||
|
40
res/menu/conversation_reactions_long_press_menu.xml
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_info"
|
||||
android:icon="?menu_info_icon"
|
||||
android:title="@string/conversation_context__menu_message_details"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:icon="?menu_trash_icon"
|
||||
android:title="@string/conversation_context__menu_delete_message"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_copy"
|
||||
android:icon="?menu_copy_icon"
|
||||
android:title="@string/conversation_context__menu_copy_text"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_reply"
|
||||
android:icon="?menu_reply_icon"
|
||||
android:title="@string/conversation_context__menu_reply_to_message"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:visible="false"
|
||||
android:id="@+id/action_download"
|
||||
android:icon="?menu_save_icon"
|
||||
android:title="@string/conversation_context_image__save_attachment"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_multiselect"
|
||||
android:icon="?menu_multi_select_icon"
|
||||
android:title="@string/conversation_context__reaction_multi_select"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_forward"
|
||||
android:icon="?menu_forward_icon"
|
||||
android:title="@string/conversation_context__menu_forward_message"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
@ -7,7 +7,7 @@
|
||||
|
||||
<item android:id="@+id/delete"
|
||||
android:title="@string/delete"
|
||||
android:icon="@drawable/ic_delete_white_24dp"
|
||||
android:icon="?menu_trash_icon"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item android:id="@+id/select_all"
|
||||
|
@ -3,7 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/media_preview__forward"
|
||||
android:title="@string/media_preview__forward_title"
|
||||
android:icon="@drawable/ic_forward_white_24dp"
|
||||
android:icon="?menu_forward_icon"
|
||||
app:showAsAction="always"/>
|
||||
<item android:id="@+id/save"
|
||||
android:title="@string/media_preview__save_title"
|
||||
@ -15,6 +15,6 @@
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item android:id="@+id/delete"
|
||||
android:title="@string/delete"
|
||||
android:icon="@drawable/ic_delete_white_24dp"
|
||||
android:icon="?menu_trash_icon"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
|
@ -92,6 +92,12 @@
|
||||
<attr name="emoji_category_emoticons" format="reference"/>
|
||||
<attr name="emoji_variation_selector_background" format="reference|color" />
|
||||
|
||||
<attr name="reactions_sent_background" format="reference" />
|
||||
<attr name="reactions_recv_background" format="reference" />
|
||||
<attr name="reactions_overlay_old_background" format="reference" />
|
||||
<attr name="reactions_overlay_scrubber_background" format="reference" />
|
||||
<attr name="reactions_bottom_dialog_fragment_emoji_selected" format="reference" />
|
||||
|
||||
<attr name="camera_button_style" />
|
||||
|
||||
<attr name="quick_camera_icon" format="reference"/>
|
||||
@ -172,6 +178,7 @@
|
||||
<attr name="menu_forward_icon" format="reference" />
|
||||
<attr name="menu_save_icon" format="reference" />
|
||||
<attr name="menu_reply_icon" format="reference" />
|
||||
<attr name="menu_multi_select_icon" format="reference" />
|
||||
|
||||
<attr name="message_icon" format="reference" />
|
||||
<attr name="notifications_icon" format="reference" />
|
||||
|
@ -16,8 +16,10 @@
|
||||
<color name="transparent_black_20">#33000000</color>
|
||||
<color name="transparent_black_40">#66000000</color>
|
||||
<color name="transparent_black_60">#99000000</color>
|
||||
<color name="transparent_black_80">#CC000000</color>
|
||||
|
||||
<color name="transparent_white_20">#33ffffff</color>
|
||||
<color name="transparent_white_30">#4Dffffff</color>
|
||||
<color name="transparent_white_60">#99ffffff</color>
|
||||
<color name="transparent_white_80">#ccffffff</color>
|
||||
<color name="transparent_white_90">#e6ffffff</color>
|
||||
|
@ -46,6 +46,12 @@
|
||||
<dimen name="media_bubble_max_height">320dp</dimen>
|
||||
<dimen name="media_bubble_sticker_dimens">128dp</dimen>
|
||||
|
||||
<dimen name="reactions_bubble_margin">-37dp</dimen>
|
||||
<dimen name="reactions_bubble_size">32dp</dimen>
|
||||
<dimen name="reactions_bubble_text_size">16dp</dimen>
|
||||
<dimen name="reactions_bubble_margin_max">28dp</dimen>
|
||||
<dimen name="reactions_bubble_container_max_height">60dp</dimen>
|
||||
|
||||
<dimen name="media_picker_folder_width">175dp</dimen>
|
||||
<dimen name="media_picker_item_width">85dp</dimen>
|
||||
<dimen name="media_picker_rail_padding_affordance">130dp</dimen>
|
||||
@ -64,6 +70,11 @@
|
||||
<dimen name="conversation_vertical_message_spacing_default">8dp</dimen>
|
||||
<dimen name="conversation_vertical_message_spacing_collapse">1dp</dimen>
|
||||
|
||||
<dimen name="conversation_reaction_picker_emoji_text_size">28dp</dimen>
|
||||
|
||||
<dimen name="reaction_scrubber_anim_start_translation_y">25dp</dimen>
|
||||
<dimen name="reaction_scrubber_anim_end_translation_y">0dp</dimen>
|
||||
|
||||
<dimen name="conversation_item_reply_size">20dp</dimen>
|
||||
<dimen name="conversation_item_avatar_size">28dp</dimen>
|
||||
|
||||
@ -129,4 +140,12 @@
|
||||
|
||||
<dimen name="contact_selection_item_height">@dimen/selection_item_header_height</dimen>
|
||||
|
||||
<dimen name="conversation_reaction_scrubber_height">136dp</dimen>
|
||||
<dimen name="conversation_reaction_scrubber_distance">40dp</dimen>
|
||||
<dimen name="conversation_reaction_touch_deadzone_size">60dp</dimen>
|
||||
<dimen name="conversation_reaction_scrub_deadzone_distance_from_touch_top">136dp</dimen>
|
||||
<dimen name="conversation_reaction_scrub_deadzone_distance_from_touch_bottom">40dp</dimen>
|
||||
<dimen name="conversation_reaction_scrub_vertical_translation">20dp</dimen>
|
||||
<dimen name="conversation_reaction_scrub_horizontal_margin">16dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
@ -1,3 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="emoji_heart" translatable="false">\u2764\ufe0f</string>
|
||||
<string name="emoji_thumbs_up" translatable="false">\ud83d\udc4d</string>
|
||||
<string name="emoji_thumbs_down" translatable="false">\ud83d\udc4e</string>
|
||||
<string name="emoji_laugh" translatable="false">\ud83d\ude02</string>
|
||||
<string name="emoji_surprise" translatable="false">\ud83d\ude2e</string>
|
||||
<string name="emoji_sad" translatable="false">\ud83d\ude22</string>
|
||||
<string name="emoji_angry" translatable="false">\ud83d\ude21</string>
|
||||
</resources>
|
||||
|
@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="play_button_animation_duration">300</integer>
|
||||
<integer name="reaction_scrubber_reveal_duration">400</integer>
|
||||
<integer name="reaction_scrubber_reveal_emoji_duration">380</integer>
|
||||
<integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</integer>
|
||||
</resources>
|
@ -894,6 +894,7 @@
|
||||
<string name="MessageNotifier_open_signal_to_check_for_recent_notifications">Open Signal to check for recent notifications.</string>
|
||||
<string name="MessageNotifier_contact_message">%1$s %2$s</string>
|
||||
<string name="MessageNotifier_unknown_contact_message">Contact</string>
|
||||
<string name="MessageNotifier_reacted_to_your_message">Reacted to your message: %1$s</string>
|
||||
|
||||
<!-- Notification Channels -->
|
||||
<string name="NotificationChannel_messages">Default</string>
|
||||
@ -1467,6 +1468,9 @@
|
||||
<string name="conversation_context__menu_resend_message">Resend message</string>
|
||||
<string name="conversation_context__menu_reply_to_message">Reply to message</string>
|
||||
|
||||
<!-- conversation_context_reacction -->
|
||||
<string name="conversation_context__reaction_multi_select">Select multiple</string>
|
||||
|
||||
<!-- conversation_context_image -->
|
||||
<string name="conversation_context_image__save_attachment">Save attachment</string>
|
||||
|
||||
|
@ -397,4 +397,8 @@
|
||||
<style name="Signal.MessageRequest.Button.Accept">
|
||||
<item name="android:textColor">@color/core_blue</item>
|
||||
</style>
|
||||
|
||||
<declare-styleable name="MaxHeightFrameLayout">
|
||||
<attr name="mhfl_maxHeight" format="dimension" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
@ -162,6 +162,8 @@
|
||||
<item name="title_text_color_primary">@color/core_grey_90</item>
|
||||
<item name="title_text_color_secondary">@color/core_grey_60</item>
|
||||
|
||||
<item name="bottomSheetDialogTheme">@style/Theme.Design.Light.BottomSheetDialog</item>
|
||||
|
||||
<item name="actionBarStyle">@style/TextSecure.LightActionBar</item>
|
||||
<item name="actionBarTabBarStyle">@style/TextSecure.LightActionBar.TabBar</item>
|
||||
<item name="actionModeBackground">@color/core_grey_50</item>
|
||||
@ -257,6 +259,12 @@
|
||||
<item name="emoji_category_emoticons">@drawable/ic_emoji_emoticon_light_20</item>
|
||||
<item name="emoji_variation_selector_background">@drawable/emoji_variation_selector_background_light</item>
|
||||
|
||||
<item name="reactions_recv_background">@drawable/reactions_recv_background_light</item>
|
||||
<item name="reactions_sent_background">@drawable/reactions_send_background_light</item>
|
||||
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
||||
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
||||
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_light</item>
|
||||
|
||||
<item name="conversation_item_bubble_background">@color/core_grey_05</item>
|
||||
<item name="conversation_item_sent_text_primary_color">@color/core_grey_90</item>
|
||||
<item name="conversation_item_sent_text_secondary_color">@color/core_grey_60</item>
|
||||
@ -307,16 +315,17 @@
|
||||
<item name="menu_unlock_icon">@drawable/ic_unlocked_white_24dp</item>
|
||||
<item name="menu_lock_icon">@drawable/ic_lock_white_24dp</item>
|
||||
<item name="menu_lock_icon_small">@drawable/ic_lock_white_18dp</item>
|
||||
<item name="menu_trash_icon">@drawable/ic_delete_white_24dp</item>
|
||||
<item name="menu_trash_icon">@drawable/ic_trash_outline_24</item>
|
||||
<item name="menu_selectall_icon">@drawable/ic_select_all_white_24dp</item>
|
||||
<item name="menu_split_icon">@drawable/ic_call_split_white_24dp</item>
|
||||
<item name="menu_accept_icon">@drawable/ic_check_24</item>
|
||||
<item name="menu_refresh_directory">@drawable/ic_refresh_white_24dp</item>
|
||||
<item name="menu_copy_icon">@drawable/ic_content_copy_white_24dp</item>
|
||||
<item name="menu_copy_icon">@drawable/ic_copy_outline_24</item>
|
||||
<item name="menu_info_icon">@drawable/ic_info_outline_white_24dp</item>
|
||||
<item name="menu_forward_icon">@drawable/ic_forward_white_24dp</item>
|
||||
<item name="menu_save_icon">@drawable/ic_download_filled_white_24</item>
|
||||
<item name="menu_reply_icon">@drawable/ic_reply_white_24dp</item>
|
||||
<item name="menu_forward_icon">@drawable/ic_forward_outline_24</item>
|
||||
<item name="menu_save_icon">@drawable/ic_save_24</item>
|
||||
<item name="menu_reply_icon">@drawable/ic_reply_outline_24</item>
|
||||
<item name="menu_multi_select_icon">@drawable/ic_select_24</item>
|
||||
|
||||
<item name="message_icon">@drawable/ic_message_outline_tinted_24</item>
|
||||
<item name="notifications_icon">@drawable/ic_bell_outline_24</item>
|
||||
@ -392,6 +401,8 @@
|
||||
<item name="title_text_color_primary">@color/core_grey_05</item>
|
||||
<item name="title_text_color_secondary">@color/core_grey_25</item>
|
||||
|
||||
<item name="bottomSheetDialogTheme">@style/Theme.Design.BottomSheetDialog</item>
|
||||
|
||||
<item name="actionBarStyle">@style/TextSecure.DarkActionBar</item>
|
||||
<item name="actionBarTabBarStyle">@style/TextSecure.DarkActionBar.TabBar</item>
|
||||
<item name="actionBarPopupTheme">@style/ThemeOverlay.AppCompat.Dark</item>
|
||||
@ -500,6 +511,13 @@
|
||||
<item name="conversation_title_color">@color/transparent_white_90</item>
|
||||
<item name="conversation_subtitle_color">@color/transparent_white_80</item>
|
||||
|
||||
<item name="reactions_recv_background">@drawable/reactions_recv_background_dark</item>
|
||||
<item name="reactions_sent_background">@drawable/reactions_send_background_dark</item>
|
||||
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
||||
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
||||
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_dark</item>
|
||||
|
||||
|
||||
<item name="emoji_tab_strip_background">@color/core_grey_85</item>
|
||||
<item name="emoji_tab_indicator">@color/core_grey_65</item>
|
||||
<item name="emoji_tab_underline">@color/core_grey_75</item>
|
||||
@ -537,16 +555,17 @@
|
||||
<item name="menu_unlock_icon">@drawable/ic_unlocked_white_24dp</item>
|
||||
<item name="menu_lock_icon">@drawable/ic_lock_white_24dp</item>
|
||||
<item name="menu_lock_icon_small">@drawable/ic_lock_white_18dp</item>
|
||||
<item name="menu_trash_icon">@drawable/ic_delete_white_24dp</item>
|
||||
<item name="menu_trash_icon">@drawable/ic_trash_solid_24</item>
|
||||
<item name="menu_selectall_icon">@drawable/ic_select_all_white_24dp</item>
|
||||
<item name="menu_split_icon">@drawable/ic_call_split_white_24dp</item>
|
||||
<item name="menu_accept_icon">@drawable/ic_check_24</item>
|
||||
<item name="menu_refresh_directory">@drawable/ic_refresh_white_24dp</item>
|
||||
<item name="menu_copy_icon">@drawable/ic_content_copy_white_24dp</item>
|
||||
<item name="menu_info_icon">@drawable/ic_info_outline_white_24dp</item>
|
||||
<item name="menu_forward_icon">@drawable/ic_forward_white_24dp</item>
|
||||
<item name="menu_save_icon">@drawable/ic_download_filled_white_24</item>
|
||||
<item name="menu_reply_icon">@drawable/ic_reply_white_24dp</item>
|
||||
<item name="menu_copy_icon">@drawable/ic_copy_solid_24</item>
|
||||
<item name="menu_info_icon">@drawable/ic_info_solid_24</item>
|
||||
<item name="menu_forward_icon">@drawable/ic_forward_solid_24</item>
|
||||
<item name="menu_save_icon">@drawable/ic_save_24</item>
|
||||
<item name="menu_reply_icon">@drawable/ic_reply_solid_24</item>
|
||||
<item name="menu_multi_select_icon">@drawable/ic_select_24</item>
|
||||
|
||||
<item name="message_icon">@drawable/ic_message_solid_tinted_24</item>
|
||||
<item name="notifications_icon">@drawable/ic_bell_solid_24</item>
|
||||
|
@ -7,6 +7,7 @@ import android.view.View;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@ -43,5 +44,6 @@ public interface BindableConversationItem extends Unbindable {
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
}
|
||||
}
|
||||
|
81
src/org/thoughtcrime/securesms/components/MaskView.java
Normal file
@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class MaskView extends View {
|
||||
|
||||
private View target;
|
||||
private int[] targetLocation = new int[2];
|
||||
private int statusBarHeight;
|
||||
private Paint maskPaint;
|
||||
|
||||
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
|
||||
|
||||
public MaskView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MaskView(@NonNull Context context, @Nullable AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
setLayerType(LAYER_TYPE_HARDWARE, maskPaint);
|
||||
|
||||
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
|
||||
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
}
|
||||
|
||||
public void setTarget(@Nullable View target) {
|
||||
if (this.target != null) {
|
||||
this.target.getViewTreeObserver().removeOnDrawListener(onDrawListener);
|
||||
}
|
||||
|
||||
this.target = target;
|
||||
|
||||
if (this.target != null) {
|
||||
this.target.getViewTreeObserver().addOnDrawListener(onDrawListener);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.getLocationInWindow(targetLocation);
|
||||
|
||||
Bitmap mask = Bitmap.createBitmap(target.getWidth(), target.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas maskCanvas = new Canvas(mask);
|
||||
|
||||
target.draw(maskCanvas);
|
||||
|
||||
canvas.drawBitmap(mask, 0, targetLocation[1] - statusBarHeight, maskPaint);
|
||||
|
||||
mask.recycle();
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class MaxHeightFrameLayout extends FrameLayout {
|
||||
|
||||
private final int maxHeight;
|
||||
|
||||
public MaxHeightFrameLayout(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightFrameLayout);
|
||||
|
||||
maxHeight = a.getDimensionPixelSize(R.styleable.MaxHeightFrameLayout_mhfl_maxHeight, 0);
|
||||
|
||||
a.recycle();
|
||||
} else {
|
||||
maxHeight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, Math.min(bottom, top + maxHeight));
|
||||
}
|
||||
}
|
@ -49,6 +49,7 @@ import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnFocusChangeListener;
|
||||
@ -145,6 +146,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
@ -311,6 +313,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private ConversationSearchBottomBar searchNav;
|
||||
private MenuItem searchViewItem;
|
||||
private FrameLayout messageRequestOverlay;
|
||||
private ConversationReactionOverlay reactionOverlay;
|
||||
|
||||
private AttachmentTypeSelector attachmentTypeSelector;
|
||||
private AttachmentManager attachmentManager;
|
||||
@ -524,6 +527,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
return reactionOverlay.applyTouchEvent(ev) || super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int reqCode, int resultCode, Intent data) {
|
||||
Log.i(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data);
|
||||
@ -1581,6 +1589,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
|
||||
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
|
||||
messageRequestOverlay = ViewUtil.findById(this, R.id.fragment_overlay_container);
|
||||
reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
|
||||
|
||||
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
|
||||
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
|
||||
@ -1636,6 +1645,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
searchNav.setEventListener(this);
|
||||
|
||||
inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment());
|
||||
|
||||
reactionOverlay.setOnReactionSelectedListener(this::onReactionSelected);
|
||||
}
|
||||
|
||||
protected void initializeActionBar() {
|
||||
@ -1748,6 +1759,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
.show(TooltipPopup.POSITION_ABOVE);
|
||||
}
|
||||
|
||||
|
||||
private void onReactionSelected(MessageRecord messageRecord, String emoji) {
|
||||
final Context context = getApplicationContext();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
|
||||
.filter(record -> record.getAuthor().equals(Recipient.self().getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) {
|
||||
MessageSender.sendReactionRemoval(context, messageRecord.getId(), messageRecord.isMms(), oldRecord);
|
||||
} else {
|
||||
MessageSender.sendNewReaction(context, messageRecord.getId(), messageRecord.isMms(), emoji);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchMoveUpPressed() {
|
||||
searchViewModel.onMoveUp();
|
||||
@ -2753,6 +2782,33 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
});
|
||||
}
|
||||
|
||||
public void handleReaction(@NonNull View maskTarget,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
|
||||
{
|
||||
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionOverlay.setOnHideListener(onHideListener);
|
||||
reactionOverlay.show(this, maskTarget, messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionOverlay.isShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
//noinspection CodeBlock2Expr
|
||||
return DatabaseFactory.getMmsSmsDatabase(this)
|
||||
.checkMessageExists(reactionOverlay.getMessageRecord());
|
||||
}, messageExists -> {
|
||||
if (!messageExists) {
|
||||
reactionOverlay.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThreadId(long threadId) {
|
||||
this.threadId = threadId;
|
||||
|
@ -139,7 +139,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
|
||||
interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
void onItemClick(MessageRecord item);
|
||||
void onItemLongClick(MessageRecord item);
|
||||
void onItemLongClick(View maskTarget, MessageRecord item);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@ -224,7 +224,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
});
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onItemLongClick(itemView.getMessageRecord());
|
||||
clickListener.onItemLongClick(itemView, itemView.getMessageRecord());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
@ -26,19 +26,6 @@ import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
import android.text.ClipboardManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
@ -54,7 +41,23 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ViewSwitcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
@ -91,10 +94,10 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@ -146,6 +149,7 @@ public class ConversationFragment extends Fragment
|
||||
private int previousOffset;
|
||||
private int activeOffset;
|
||||
private boolean firstLoad;
|
||||
private boolean isReacting;
|
||||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
private RecyclerView list;
|
||||
@ -326,7 +330,9 @@ public class ConversationFragment extends Fragment
|
||||
if (!isTypingIndicatorShowing() && isAtBottom()) {
|
||||
Context context = requireContext();
|
||||
list.setVerticalScrollBarEnabled(false);
|
||||
list.post(() -> getListLayoutManager().smoothScrollToPosition(context, 0, 250));
|
||||
list.post(() -> { if (!isReacting) {
|
||||
getListLayoutManager().smoothScrollToPosition(context, 0, 250);
|
||||
}});
|
||||
list.postDelayed(() -> list.setVerticalScrollBarEnabled(true), 300);
|
||||
adapter.setHeaderView(typingView);
|
||||
adapter.notifyItemInserted(0);
|
||||
@ -341,7 +347,7 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
} else {
|
||||
if (getListLayoutManager().findFirstCompletelyVisibleItemPosition() == 0 && getListLayoutManager().getItemCount() > 1 && !replacedByIncomingMessage) {
|
||||
getListLayoutManager().smoothScrollToPosition(requireContext(), 1, 250);
|
||||
if (!isReacting) getListLayoutManager().smoothScrollToPosition(requireContext(), 1, 250);
|
||||
list.setVerticalScrollBarEnabled(false);
|
||||
list.postDelayed(() -> {
|
||||
adapter.setHeaderView(null);
|
||||
@ -707,8 +713,8 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
|
||||
activeOffset = loader.getOffset();
|
||||
|
||||
adapter.changeCursor(cursor);
|
||||
listener.onCursorChanged();
|
||||
|
||||
int lastSeenPosition = adapter.findLastSeenPosition(lastSeen);
|
||||
|
||||
@ -749,6 +755,7 @@ public class ConversationFragment extends Fragment
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> arg0) {
|
||||
if (list.getAdapter() != null) {
|
||||
getListAdapter().changeCursor(null);
|
||||
listener.onCursorChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@ -866,6 +873,11 @@ public class ConversationFragment extends Fragment
|
||||
void onMessageActionToolbarOpened();
|
||||
void onForwardClicked();
|
||||
void onMessageRequest();
|
||||
void handleReaction(@NonNull View maskTarget,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
||||
void onCursorChanged();
|
||||
}
|
||||
|
||||
private class ConversationScrollListener extends OnScrollListener {
|
||||
@ -955,8 +967,21 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemLongClick(MessageRecord messageRecord) {
|
||||
if (actionMode == null) {
|
||||
public void onItemLongClick(View maskTarget, MessageRecord messageRecord) {
|
||||
|
||||
if (actionMode != null) return;
|
||||
|
||||
if (FeatureFlags.REACTION_SENDING &&
|
||||
messageRecord.isSecure() &&
|
||||
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
|
||||
{
|
||||
isReacting = true;
|
||||
list.setLayoutFrozen(true);
|
||||
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(messageRecord), () -> {
|
||||
isReacting = false;
|
||||
list.setLayoutFrozen(false);
|
||||
});
|
||||
} else {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
@ -1094,6 +1119,13 @@ public class ConversationFragment extends Fragment
|
||||
CommunicationActions.composeSmsThroughDefaultApp(getContext(), recipient, getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactionClicked(long messageId, boolean isMms) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1105,6 +1137,36 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEnterMultiSelect(@NonNull MessageRecord messageRecord) {
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
}
|
||||
|
||||
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
|
||||
private ReactionsToolbarListener(@NonNull MessageRecord messageRecord) {
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_info: handleDisplayDetails(messageRecord); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(messageRecord)); return true;
|
||||
case R.id.action_copy: handleCopyMessage(Sets.newHashSet(messageRecord)); return true;
|
||||
case R.id.action_reply: handleReplyMessage(messageRecord); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(messageRecord); return true;
|
||||
case R.id.action_forward: handleForwardMessage(messageRecord); return true;
|
||||
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) messageRecord); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
private int statusBarColor;
|
||||
|
@ -26,11 +26,6 @@ import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@ -45,15 +40,21 @@ import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.ConfirmIdentityDialog;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
@ -159,6 +160,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private AvatarImageView contactPhoto;
|
||||
private AlertView alertView;
|
||||
private ViewGroup container;
|
||||
protected ViewGroup reactionsContainer;
|
||||
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
@ -169,8 +171,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private Stub<SharedContactView> sharedContactStub;
|
||||
private Stub<LinkPreviewView> linkPreviewStub;
|
||||
private Stub<StickerView> stickerStub;
|
||||
private Stub<ViewOnceMessageView> revealableStub;
|
||||
private Stub<ViewOnceMessageView> revealableStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
private ConversationItemReactionBubbles conversationItemReactionBubbles;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
private int measureCalls;
|
||||
@ -225,6 +228,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.container = findViewById(R.id.container);
|
||||
this.reply = findViewById(R.id.reply_icon);
|
||||
this.reactionsContainer = findViewById(R.id.reactions_bubbles_container);
|
||||
|
||||
this.conversationItemReactionBubbles = new ConversationItemReactionBubbles(this.reactionsContainer);
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
|
||||
@ -274,6 +280,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setReactions(messageRecord);
|
||||
setFooter(messageRecord, nextMessageRecord, locale, groupThread);
|
||||
}
|
||||
|
||||
@ -369,7 +376,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
@Override
|
||||
public void unbind() {
|
||||
if (recipient != null) {
|
||||
recipient.removeForeverObserver(this);;
|
||||
recipient.removeForeverObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -900,6 +907,15 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
}
|
||||
|
||||
private void setReactions(@NonNull MessageRecord current) {
|
||||
conversationItemReactionBubbles.setReactions(current.getReactions());
|
||||
reactionsContainer.setOnClickListener(v -> {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onReactionClicked(current.getId(), current.isMms());
|
||||
});
|
||||
}
|
||||
|
||||
private void setFooter(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, @NonNull Locale locale, boolean isGroupThread) {
|
||||
ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
|
||||
|
@ -0,0 +1,178 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class ConversationItemReactionBubbles {
|
||||
|
||||
private final ViewGroup reactionsContainer;
|
||||
private final EmojiTextView primaryEmojiReaction;
|
||||
private final EmojiTextView secondaryEmojiReaction;
|
||||
|
||||
ConversationItemReactionBubbles(@NonNull ViewGroup reactionsContainer) {
|
||||
this.reactionsContainer = reactionsContainer;
|
||||
this.primaryEmojiReaction = reactionsContainer.findViewById(R.id.reactions_bubbles_primary);
|
||||
this.secondaryEmojiReaction = reactionsContainer.findViewById(R.id.reactions_bubbles_secondary);
|
||||
}
|
||||
|
||||
void setReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
if (reactions.size() == 0) {
|
||||
hideAllReactions();
|
||||
return;
|
||||
}
|
||||
|
||||
final Collection<ReactionInfo> reactionInfos = getReactionInfos(reactions);
|
||||
|
||||
if (reactionInfos.size() == 1) {
|
||||
displaySingleReaction(reactionInfos.iterator().next());
|
||||
} else {
|
||||
displayMultipleReactions(reactionInfos);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Collection<ReactionInfo> getReactionInfos(@NonNull List<ReactionRecord> reactions) {
|
||||
final Map<String, ReactionInfo> counters = new HashMap<>();
|
||||
|
||||
for (ReactionRecord reaction : reactions) {
|
||||
|
||||
ReactionInfo info = counters.get(reaction.getEmoji());
|
||||
if (info == null) {
|
||||
info = new ReactionInfo(reaction.getEmoji(),
|
||||
1,
|
||||
reaction.getDateReceived(),
|
||||
Recipient.self().getId().equals(reaction.getAuthor()));
|
||||
} else {
|
||||
info = new ReactionInfo(reaction.getEmoji(),
|
||||
info.count + 1,
|
||||
Math.max(info.lastSeen, reaction.getDateReceived()),
|
||||
info.userWasSender || Recipient.self().getId().equals(reaction.getAuthor()));
|
||||
}
|
||||
|
||||
counters.put(reaction.getEmoji(), info);
|
||||
}
|
||||
|
||||
return counters.values();
|
||||
}
|
||||
|
||||
private void hideAllReactions() {
|
||||
reactionsContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void displaySingleReaction(@NonNull ReactionInfo reactionInfo) {
|
||||
reactionsContainer.setVisibility(View.VISIBLE);
|
||||
primaryEmojiReaction.setVisibility(View.VISIBLE);
|
||||
secondaryEmojiReaction.setVisibility(View.GONE);
|
||||
|
||||
primaryEmojiReaction.setText(reactionInfo.emoji);
|
||||
primaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(reactionInfo));
|
||||
}
|
||||
|
||||
private void displayMultipleReactions(@NonNull Collection<ReactionInfo> reactionInfos) {
|
||||
reactionsContainer.setVisibility(View.VISIBLE);
|
||||
primaryEmojiReaction.setVisibility(View.VISIBLE);
|
||||
secondaryEmojiReaction.setVisibility(View.VISIBLE);
|
||||
|
||||
Pair<ReactionInfo, ReactionInfo> primaryAndSecondaryReactions = getPrimaryAndSecondaryReactions(reactionInfos);
|
||||
primaryEmojiReaction.setText(primaryAndSecondaryReactions.first.emoji);
|
||||
primaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(primaryAndSecondaryReactions.first));
|
||||
secondaryEmojiReaction.setText(primaryAndSecondaryReactions.second.emoji);
|
||||
secondaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(primaryAndSecondaryReactions.second));
|
||||
}
|
||||
|
||||
private Drawable getBackgroundDrawableForReactionBubble(@NonNull ReactionInfo reactionInfo) {
|
||||
return ThemeUtil.getThemedDrawable(reactionsContainer.getContext(),
|
||||
reactionInfo.userWasSender ? R.attr.reactions_sent_background : R.attr.reactions_recv_background);
|
||||
}
|
||||
|
||||
private Pair<ReactionInfo, ReactionInfo> getPrimaryAndSecondaryReactions(@NonNull Collection<ReactionInfo> reactionInfos) {
|
||||
ReactionInfo mostPopular = null;
|
||||
ReactionInfo latestReaction = null;
|
||||
ReactionInfo secondLatestReaction = null;
|
||||
ReactionInfo ourReaction = null;
|
||||
|
||||
for (ReactionInfo current : reactionInfos) {
|
||||
if (current.userWasSender) {
|
||||
ourReaction = current;
|
||||
}
|
||||
|
||||
if (mostPopular == null) {
|
||||
mostPopular = current;
|
||||
} else if (mostPopular.count < current.count) {
|
||||
mostPopular = current;
|
||||
}
|
||||
|
||||
if (latestReaction == null) {
|
||||
latestReaction = current;
|
||||
} else if (latestReaction.lastSeen < current.lastSeen) {
|
||||
|
||||
if (current.count == mostPopular.count) {
|
||||
mostPopular = current;
|
||||
}
|
||||
|
||||
secondLatestReaction = latestReaction;
|
||||
latestReaction = current;
|
||||
} else if (secondLatestReaction == null) {
|
||||
secondLatestReaction = current;
|
||||
}
|
||||
}
|
||||
|
||||
if (mostPopular == null) {
|
||||
throw new AssertionError("getPrimaryAndSecondaryReactions was called with an empty list.");
|
||||
}
|
||||
|
||||
if (ourReaction != null && !mostPopular.equals(ourReaction)) {
|
||||
return Pair.create(mostPopular, ourReaction);
|
||||
} else {
|
||||
return Pair.create(mostPopular, mostPopular.equals(latestReaction) ? secondLatestReaction : latestReaction);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReactionInfo {
|
||||
private final String emoji;
|
||||
private final int count;
|
||||
private final long lastSeen;
|
||||
private final boolean userWasSender;
|
||||
|
||||
private ReactionInfo(@NonNull String emoji, int count, long lastSeen, boolean userWasSender) {
|
||||
this.emoji = emoji;
|
||||
this.count = count;
|
||||
this.lastSeen = lastSeen;
|
||||
this.userWasSender = userWasSender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(emoji, count, lastSeen, userWasSender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof ReactionInfo)) return false;
|
||||
|
||||
ReactionInfo other = (ReactionInfo) obj;
|
||||
|
||||
return other.emoji.equals(emoji) &&
|
||||
other.count == count &&
|
||||
other.lastSeen == lastSeen &&
|
||||
other.userWasSender == userWasSender;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,551 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.MaskView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
|
||||
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
|
||||
|
||||
private final Rect emojiViewGlobalRect = new Rect();
|
||||
private final Rect emojiStripViewBounds = new Rect();
|
||||
private float segmentSize;
|
||||
|
||||
private final Boundary horizontalEmojiBoundary = new Boundary();
|
||||
private final Boundary verticalScrubBoundary = new Boundary();
|
||||
private final PointF deadzoneTouchPoint = new PointF();
|
||||
private final PointF lastSeenDownPoint = new PointF();
|
||||
|
||||
private Activity activity;
|
||||
private MessageRecord messageRecord;
|
||||
private OverlayState overlayState = OverlayState.HIDDEN;
|
||||
|
||||
private boolean downIsOurs;
|
||||
private boolean isToolbarTouch;
|
||||
private int selected = -1;
|
||||
private int originalStatusBarColor;
|
||||
|
||||
private View backgroundView;
|
||||
private ConstraintLayout foregroundView;
|
||||
private View selectedView;
|
||||
private View[] emojiViews;
|
||||
private MaskView maskView;
|
||||
private Toolbar toolbar;
|
||||
|
||||
private int statusBarHeight;
|
||||
private float touchDownDeadZoneSize;
|
||||
private float distanceFromTouchDownPointToTopOfScrubberDeadZone;
|
||||
private float distanceFromTouchDownPointToBottomOfScrubberDeadZone;
|
||||
private int scrubberDistanceFromTouchDown;
|
||||
private int scrubberHeight;
|
||||
private int halfActionBarHeight;
|
||||
private int selectedVerticalTranslation;
|
||||
private int scrubberHorizontalMargin;
|
||||
private int animationEmojiStartDelayFactor;
|
||||
|
||||
private OnReactionSelectedListener onReactionSelectedListener;
|
||||
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
|
||||
private OnHideListener onHideListener;
|
||||
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
|
||||
public ConversationReactionOverlay(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ConversationReactionOverlay(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator);
|
||||
maskView = findViewById(R.id.conversation_reaction_mask);
|
||||
toolbar = findViewById(R.id.conversation_reaction_toolbar);
|
||||
|
||||
toolbar.setOnMenuItemClickListener(this::handleToolbarItemClicked);
|
||||
toolbar.setNavigationOnClickListener(view -> hide());
|
||||
|
||||
emojiViews = Stream.of(ReactionEmoji.values())
|
||||
.map(e -> {
|
||||
EmojiTextView view = findViewById(e.viewId);
|
||||
view.setText(e.emoji);
|
||||
return view;
|
||||
}).toArray(View[]::new);
|
||||
|
||||
distanceFromTouchDownPointToTopOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_top);
|
||||
distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom);
|
||||
|
||||
touchDownDeadZoneSize = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size);
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
scrubberDistanceFromTouchDown = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_distance);
|
||||
scrubberHeight = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_height);
|
||||
halfActionBarHeight = (int) ThemeUtil.getThemedDimen(getContext(), R.attr.actionBarSize) / 2;
|
||||
selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
|
||||
scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);
|
||||
|
||||
animationEmojiStartDelayFactor = getResources().getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor);
|
||||
|
||||
initAnimators();
|
||||
}
|
||||
|
||||
public void show(@NonNull Activity activity, @NonNull View maskTarget, @NonNull MessageRecord messageRecord) {
|
||||
|
||||
if (overlayState != OverlayState.HIDDEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageRecord = messageRecord;
|
||||
overlayState = OverlayState.UNINITAILIZED;
|
||||
selected = -1;
|
||||
|
||||
setupToolbarMenuItems();
|
||||
setupSelectedEmojiBackground();
|
||||
|
||||
final float scrubberTranslationY = Math.max(-scrubberDistanceFromTouchDown + halfActionBarHeight,
|
||||
lastSeenDownPoint.y - scrubberHeight - scrubberDistanceFromTouchDown);
|
||||
|
||||
final float halfWidth = foregroundView.getWidth() / 2f + scrubberHorizontalMargin;
|
||||
final float screenWidth = getResources().getDisplayMetrics().widthPixels;
|
||||
final float scrubberTranslationX = Util.clamp(lastSeenDownPoint.x - halfWidth,
|
||||
scrubberHorizontalMargin,
|
||||
screenWidth + scrubberHorizontalMargin - halfWidth * 2);
|
||||
|
||||
backgroundView.setTranslationX(scrubberTranslationX);
|
||||
backgroundView.setTranslationY(scrubberTranslationY);
|
||||
|
||||
foregroundView.setTranslationX(scrubberTranslationX);
|
||||
foregroundView.setTranslationY(scrubberTranslationY);
|
||||
|
||||
verticalScrubBoundary.update(lastSeenDownPoint.y - distanceFromTouchDownPointToTopOfScrubberDeadZone,
|
||||
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
|
||||
|
||||
maskView.setTarget(maskTarget);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
setVisibility(View.VISIBLE);
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.activity = activity;
|
||||
originalStatusBarColor = activity.getWindow().getStatusBarColor();
|
||||
activity.getWindow().setStatusBarColor(ContextCompat.getColor(activity, R.color.core_grey_45));
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
maskView.setTarget(null);
|
||||
overlayState = OverlayState.HIDDEN;
|
||||
|
||||
revealAnimatorSet.end();
|
||||
hideAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
activity.getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return overlayState != OverlayState.HIDDEN;
|
||||
}
|
||||
|
||||
public @NonNull MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
|
||||
public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
|
||||
if (!isShowing()) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
lastSeenDownPoint.set(motionEvent.getRawX(), motionEvent.getRawY());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (overlayState == OverlayState.UNINITAILIZED) {
|
||||
backgroundView.getGlobalVisibleRect(emojiStripViewBounds);
|
||||
emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect);
|
||||
emojiStripViewBounds.left = emojiViewGlobalRect.left;
|
||||
emojiViews[emojiViews.length - 1].getGlobalVisibleRect(emojiViewGlobalRect);
|
||||
emojiStripViewBounds.right = emojiViewGlobalRect.right;
|
||||
|
||||
segmentSize = emojiStripViewBounds.width() / (float) emojiViews.length;
|
||||
|
||||
downIsOurs = false;
|
||||
|
||||
deadzoneTouchPoint.set(motionEvent.getRawX(), motionEvent.getRawY());
|
||||
|
||||
overlayState = OverlayState.DEADZONE;
|
||||
}
|
||||
|
||||
if (overlayState == OverlayState.DEADZONE) {
|
||||
float deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.getRawX());
|
||||
float deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.getRawY());
|
||||
|
||||
if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) {
|
||||
overlayState = OverlayState.SCRUB;
|
||||
} else {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||
overlayState = OverlayState.TAP;
|
||||
|
||||
if (downIsOurs) {
|
||||
handleUpEvent();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return MotionEvent.ACTION_MOVE == motionEvent.getAction();
|
||||
}
|
||||
}
|
||||
|
||||
if (isToolbarTouch) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||
isToolbarTouch = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (motionEvent.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
selected = getSelectedIndexViaDownEvent(motionEvent);
|
||||
|
||||
if (selected == -1) {
|
||||
if (motionEvent.getRawY() < toolbar.getHeight() + statusBarHeight) {
|
||||
isToolbarTouch = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deadzoneTouchPoint.set(motionEvent.getRawX(), motionEvent.getRawY());
|
||||
overlayState = OverlayState.DEADZONE;
|
||||
downIsOurs = true;
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
selected = getSelectedIndexViaMoveEvent(motionEvent);
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
handleUpEvent();
|
||||
return downIsOurs;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
hide();
|
||||
return downIsOurs;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSelectedEmojiBackground() {
|
||||
final String oldEmoji = getOldEmoji(messageRecord);
|
||||
|
||||
if (oldEmoji == null) {
|
||||
selectedView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
for (int i = 0; i < emojiViews.length; i++) {
|
||||
final View view = emojiViews[i];
|
||||
view.setScaleX(1.0f);
|
||||
view.setScaleY(1.0f);
|
||||
view.setTranslationY(0);
|
||||
|
||||
if (ReactionEmoji.values()[i].emoji.equals(oldEmoji)) {
|
||||
selectedView.setVisibility(View.VISIBLE);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(foregroundView);
|
||||
constraintSet.clear(selectedView.getId(), ConstraintSet.LEFT);
|
||||
constraintSet.clear(selectedView.getId(), ConstraintSet.RIGHT);
|
||||
constraintSet.connect(selectedView.getId(), ConstraintSet.LEFT, view.getId(), ConstraintSet.LEFT);
|
||||
constraintSet.connect(selectedView.getId(), ConstraintSet.RIGHT, view.getId(), ConstraintSet.RIGHT);
|
||||
constraintSet.applyTo(foregroundView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getSelectedIndexViaDownEvent(@NonNull MotionEvent motionEvent) {
|
||||
return getSelectedIndexViaMotionEvent(motionEvent, new Boundary(emojiStripViewBounds.top, emojiStripViewBounds.bottom));
|
||||
}
|
||||
|
||||
private int getSelectedIndexViaMoveEvent(@NonNull MotionEvent motionEvent) {
|
||||
return getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary);
|
||||
}
|
||||
|
||||
private int getSelectedIndexViaMotionEvent(@NonNull MotionEvent motionEvent, @NonNull Boundary boundary) {
|
||||
int selected = -1;
|
||||
|
||||
for (int i = 0; i < emojiViews.length; i++) {
|
||||
final float emojiLeft = (segmentSize * i) + emojiStripViewBounds.left;
|
||||
horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize);
|
||||
|
||||
if (horizontalEmojiBoundary.contains(motionEvent.getRawX()) && boundary.contains(motionEvent.getRawY())) {
|
||||
selected = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selected != -1 && this.selected != selected) {
|
||||
shrinkView(emojiViews[this.selected]);
|
||||
}
|
||||
|
||||
if (this.selected != selected && selected != -1) {
|
||||
growView(emojiViews[selected]);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private void growView(@NonNull View view) {
|
||||
view.animate()
|
||||
.scaleY(1.5f)
|
||||
.scaleX(1.5f)
|
||||
.translationY(-selectedVerticalTranslation)
|
||||
.setDuration(400)
|
||||
.setInterpolator(INTERPOLATOR)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void shrinkView(@NonNull View view) {
|
||||
view.animate()
|
||||
.scaleX(1.0f)
|
||||
.scaleY(1.0f)
|
||||
.translationY(0)
|
||||
.setDuration(400)
|
||||
.setInterpolator(INTERPOLATOR)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void handleUpEvent() {
|
||||
hide();
|
||||
if (selected != -1 && onReactionSelectedListener != null) {
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, ReactionEmoji.values()[selected].emoji);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnReactionSelectedListener(@Nullable OnReactionSelectedListener onReactionSelectedListener) {
|
||||
this.onReactionSelectedListener = onReactionSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnToolbarItemClickedListener(@Nullable Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
|
||||
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
|
||||
}
|
||||
|
||||
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
|
||||
this.onHideListener = onHideListener;
|
||||
}
|
||||
|
||||
private static @Nullable String getOldEmoji(@NonNull MessageRecord messageRecord) {
|
||||
return Stream.of(messageRecord.getReactions())
|
||||
.filter(record -> record.getAuthor()
|
||||
.serialize()
|
||||
.equals(Recipient.self()
|
||||
.getId()
|
||||
.serialize()))
|
||||
.findFirst()
|
||||
.map(ReactionRecord::getEmoji)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void setupToolbarMenuItems() {
|
||||
toolbar.getMenu().findItem(R.id.action_copy).setVisible(shouldShowCopy());
|
||||
toolbar.getMenu().findItem(R.id.action_download).setVisible(shouldShowDownload());
|
||||
toolbar.getMenu().findItem(R.id.action_forward).setVisible(shouldShowForward());
|
||||
}
|
||||
|
||||
private boolean shouldShowCopy() {
|
||||
return !MessageRecordUtil.hasSticker(messageRecord) &&
|
||||
!MessageRecordUtil.isMediaMessage(messageRecord) &&
|
||||
!MessageRecordUtil.hasSharedContact(messageRecord);
|
||||
}
|
||||
|
||||
private boolean shouldShowDownload() {
|
||||
return MessageRecordUtil.isMediaMessage(messageRecord) ||
|
||||
MessageRecordUtil.hasLocation(messageRecord);
|
||||
}
|
||||
|
||||
|
||||
private boolean shouldShowForward() {
|
||||
return !MessageRecordUtil.hasSharedContact(messageRecord);
|
||||
}
|
||||
|
||||
private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) {
|
||||
|
||||
hide();
|
||||
|
||||
if (onToolbarItemClickedListener == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return onToolbarItemClickedListener.onMenuItemClick(menuItem);
|
||||
}
|
||||
|
||||
private void initAnimators() {
|
||||
|
||||
int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
|
||||
|
||||
List<Animator> reveals = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal);
|
||||
anim.setTarget(v);
|
||||
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
|
||||
return anim;
|
||||
})
|
||||
.toList();
|
||||
|
||||
Animator overlayRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
|
||||
overlayRevealAnim.setTarget(maskView);
|
||||
overlayRevealAnim.setDuration(duration);
|
||||
reveals.add(overlayRevealAnim);
|
||||
|
||||
Animator backgroundRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
|
||||
backgroundRevealAnim.setTarget(backgroundView);
|
||||
backgroundRevealAnim.setDuration(duration);
|
||||
reveals.add(backgroundRevealAnim);
|
||||
|
||||
Animator selectedRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
|
||||
selectedRevealAnim.setTarget(selectedView);
|
||||
selectedRevealAnim.setDuration(duration);
|
||||
reveals.add(selectedRevealAnim);
|
||||
|
||||
revealAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealAnimatorSet.playTogether(reveals);
|
||||
|
||||
List<Animator> hides = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
anim.setTarget(v);
|
||||
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
|
||||
return anim;
|
||||
})
|
||||
.toList();
|
||||
|
||||
Animator overlayHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
overlayHideAnim.setTarget(maskView);
|
||||
overlayHideAnim.setDuration(duration);
|
||||
hides.add(overlayHideAnim);
|
||||
|
||||
Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
backgroundHideAnim.setTarget(backgroundView);
|
||||
backgroundHideAnim.setDuration(duration);
|
||||
hides.add(backgroundHideAnim);
|
||||
|
||||
Animator selectedHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
selectedHideAnim.setTarget(selectedView);
|
||||
selectedHideAnim.setDuration(duration);
|
||||
hides.add(selectedHideAnim);
|
||||
|
||||
hideAnimatorSet.addListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
hideAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
hideAnimatorSet.playTogether(hides);
|
||||
}
|
||||
|
||||
public interface OnHideListener {
|
||||
void onHide();
|
||||
}
|
||||
|
||||
public interface OnReactionSelectedListener {
|
||||
void onReactionSelected(@NonNull MessageRecord messageRecord, String emoji);
|
||||
}
|
||||
|
||||
private static class Boundary {
|
||||
private float min;
|
||||
private float max;
|
||||
|
||||
Boundary() {}
|
||||
|
||||
Boundary(float min, float max) {
|
||||
update(min, max);
|
||||
}
|
||||
|
||||
private void update(float min, float max) {
|
||||
Preconditions.checkArgument(min < max, "Min must be less than max");
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public boolean contains(float value) {
|
||||
return this.min < value && this.max > value;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ReactionEmoji {
|
||||
HEART(R.id.reaction_1, "\u2764\ufe0f"),
|
||||
THUMBS_UP(R.id.reaction_2, "\ud83d\udc4d"),
|
||||
THUMBS_DOWN(R.id.reaction_3, "\ud83d\udc4e"),
|
||||
LAUGH(R.id.reaction_4, "\ud83d\ude02"),
|
||||
SURPRISE(R.id.reaction_5, "\ud83d\ude2e"),
|
||||
SAD(R.id.reaction_6, "\ud83d\ude22"),
|
||||
ANGRY(R.id.reaction_7, "\ud83d\ude21");
|
||||
|
||||
final @IdRes int viewId;
|
||||
final String emoji;
|
||||
|
||||
ReactionEmoji(int viewId, String emoji) {
|
||||
this.viewId = viewId;
|
||||
this.emoji = emoji;
|
||||
}
|
||||
}
|
||||
|
||||
private enum OverlayState {
|
||||
HIDDEN,
|
||||
UNINITAILIZED,
|
||||
DEADZONE,
|
||||
SCRUB,
|
||||
TAP
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ final class ConversationSwipeAnimationHelper {
|
||||
float progress = dx / TRIGGER_DX;
|
||||
|
||||
updateBodyBubbleTransition(conversationItem.bodyBubble, dx, sign);
|
||||
updateReactionsTransition(conversationItem.reactionsContainer, dx, sign);
|
||||
updateReplyIconTransition(conversationItem.reply, dx, progress, sign);
|
||||
updateContactPhotoHolderTransition(conversationItem.contactPhotoHolder, progress, sign);
|
||||
}
|
||||
@ -45,6 +46,10 @@ final class ConversationSwipeAnimationHelper {
|
||||
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));
|
||||
|
@ -116,6 +116,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@ -339,23 +340,9 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
}
|
||||
|
||||
private void initializeProfileIcon(@NonNull Recipient recipient) {
|
||||
ImageView icon = requireView().findViewById(R.id.toolbar_icon);
|
||||
String name = Optional.fromNullable(recipient.getDisplayName(requireContext())).or(Optional.fromNullable(TextSecurePreferences.getProfileName(requireContext()))).or("");
|
||||
MaterialColor fallbackColor = recipient.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
|
||||
Drawable fallback = new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(requireContext(), fallbackColor.toAvatarColor(requireContext()));
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(new ProfileContactPhoto(recipient.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(requireContext()))))
|
||||
.error(fallback)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(icon);
|
||||
ImageView icon = requireView().findViewById(R.id.toolbar_icon);
|
||||
|
||||
AvatarUtil.loadIconIntoImageView(recipient, icon);
|
||||
icon.setOnClickListener(v -> getNavigator().goToAppSettings());
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import android.database.DataSetObserver;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
|
@ -7,23 +7,29 @@ import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.DatabaseProtos.ReactionList;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class MessagingDatabase extends Database implements MmsSmsColumns {
|
||||
|
||||
@ -90,6 +96,91 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
||||
}
|
||||
|
||||
public void setReactionsSeen(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
String whereClause = THREAD_ID + " = ?";
|
||||
String[] whereArgs = new String[]{String.valueOf(threadId)};
|
||||
|
||||
values.put(REACTIONS_UNREAD, 0);
|
||||
values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
|
||||
|
||||
db.update(getTableName(), values, whereClause, whereArgs);
|
||||
}
|
||||
|
||||
public void setAllReactionsSeen() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(REACTIONS_UNREAD, 0);
|
||||
values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
|
||||
|
||||
db.update(getTableName(), values, null, null);
|
||||
}
|
||||
|
||||
public void addReaction(long messageId, @NonNull ReactionRecord reaction) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance());
|
||||
ReactionList.Reaction newReaction = ReactionList.Reaction.newBuilder()
|
||||
.setEmoji(reaction.getEmoji())
|
||||
.setAuthor(reaction.getAuthor().toLong())
|
||||
.setSentTime(reaction.getDateSent())
|
||||
.setReceivedTime(reaction.getDateReceived())
|
||||
.build();
|
||||
|
||||
ReactionList updatedList = pruneByAuthor(reactions, reaction.getAuthor()).toBuilder()
|
||||
.addReactions(newReaction)
|
||||
.build();
|
||||
|
||||
setReactions(db, messageId, updatedList);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListeners(getThreadId(db, messageId));
|
||||
}
|
||||
|
||||
public void deleteReaction(long messageId, @NonNull RecipientId author) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance());
|
||||
ReactionList updatedList = pruneByAuthor(reactions, author);
|
||||
|
||||
setReactions(db, messageId, updatedList);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListeners(getThreadId(db, messageId));
|
||||
}
|
||||
|
||||
public boolean hasReaction(long messageId, @NonNull ReactionRecord reactionRecord) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance());
|
||||
|
||||
for (ReactionList.Reaction reaction : reactions.getReactionsList()) {
|
||||
if (reactionRecord.getAuthor().toLong() == reaction.getAuthor() &&
|
||||
reactionRecord.getEmoji().equals(reaction.getEmoji()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addMismatchedIdentity(long messageId, @NonNull RecipientId recipientId, IdentityKey identityKey) {
|
||||
try {
|
||||
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
||||
@ -110,6 +201,28 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
}
|
||||
}
|
||||
|
||||
protected List<ReactionRecord> parseReactions(@NonNull Cursor cursor) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(REACTIONS));
|
||||
|
||||
if (raw != null) {
|
||||
try {
|
||||
return Stream.of(ReactionList.parseFrom(raw).getReactionsList())
|
||||
.map(r -> {
|
||||
return new ReactionRecord(r.getEmoji(),
|
||||
RecipientId.from(r.getAuthor()),
|
||||
r.getSentTime(),
|
||||
r.getReceivedTime());
|
||||
})
|
||||
.toList();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "[parseReactions] Failed to parse reaction list!", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
protected <D extends Document<I>, I> void removeFromDocument(long messageId, String column, I object, Class<D> clazz) throws IOException {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.beginTransaction();
|
||||
@ -205,6 +318,62 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull ReactionList pruneByAuthor(@NonNull ReactionList reactionList, @NonNull RecipientId recipientId) {
|
||||
List<ReactionList.Reaction> pruned = Stream.of(reactionList.getReactionsList())
|
||||
.filterNot(r -> r.getAuthor() == recipientId.toLong())
|
||||
.toList();
|
||||
|
||||
return reactionList.toBuilder()
|
||||
.clearReactions()
|
||||
.addAllReactions(pruned)
|
||||
.build();
|
||||
}
|
||||
|
||||
private @NonNull Optional<ReactionList> getReactions(SQLiteDatabase db, long messageId) {
|
||||
String[] projection = new String[]{ REACTIONS };
|
||||
String query = ID + " = ?";
|
||||
String[] args = new String[]{String.valueOf(messageId)};
|
||||
|
||||
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(REACTIONS));
|
||||
|
||||
if (raw != null) {
|
||||
return Optional.of(ReactionList.parseFrom(raw));
|
||||
}
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "[getRecipients] Failed to parse reaction list!", e);
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
private void setReactions(@NonNull SQLiteDatabase db, long messageId, @NonNull ReactionList reactionList) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(REACTIONS, reactionList.getReactionsList().isEmpty() ? null : reactionList.toByteArray());
|
||||
values.put(REACTIONS_UNREAD, reactionList.getReactionsCount() != 0 ? 1 : 0);
|
||||
|
||||
String query = ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(messageId) };
|
||||
|
||||
db.update(getTableName(), values, query, args);
|
||||
}
|
||||
|
||||
private long getThreadId(@NonNull SQLiteDatabase db, long messageId) {
|
||||
String[] projection = new String[]{ THREAD_ID };
|
||||
String query = ID + " = ?";
|
||||
String[] args = new String[]{ String.valueOf(messageId) };
|
||||
|
||||
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static class SyncMessageId {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
|
@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
@ -112,26 +113,60 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
public static final String VIEW_ONCE = "reveal_duration";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
|
||||
READ + " INTEGER DEFAULT 0, " + "m_id" + " TEXT, " + "sub" + " TEXT, " +
|
||||
"sub_cs" + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " +
|
||||
"ct_t" + " TEXT, " + CONTENT_LOCATION + " TEXT, " + RECIPIENT_ID + " INTEGER, " +
|
||||
ADDRESS_DEVICE_ID + " INTEGER, " +
|
||||
EXPIRY + " INTEGER, " + "m_cls" + " TEXT, " + MESSAGE_TYPE + " INTEGER, " +
|
||||
"v" + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + "pri" + " INTEGER, " +
|
||||
"rr" + " INTEGER, " + "rpt_a" + " INTEGER, " + "resp_st" + " INTEGER, " +
|
||||
STATUS + " INTEGER, " + TRANSACTION_ID + " TEXT, " + "retr_st" + " INTEGER, " +
|
||||
"retr_txt" + " TEXT, " + "retr_txt_cs" + " INTEGER, " + "read_status" + " INTEGER, " +
|
||||
"ct_cls" + " INTEGER, " + "resp_txt" + " TEXT, " + "d_tm" + " INTEGER, " +
|
||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
|
||||
NETWORK_FAILURE + " TEXT DEFAULT NULL," + "d_rpt" + " INTEGER, " +
|
||||
SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||
EXPIRE_STARTED + " INTEGER DEFAULT 0, " + NOTIFIED + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + QUOTE_ID + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_AUTHOR + " TEXT, " + QUOTE_BODY + " TEXT, " + QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
|
||||
QUOTE_MISSING + " INTEGER DEFAULT 0, " + SHARED_CONTACTS + " TEXT, " + UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
LINK_PREVIEWS + " TEXT, " + VIEW_ONCE + " INTEGER DEFAULT 0);";
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
THREAD_ID + " INTEGER, " +
|
||||
DATE_SENT + " INTEGER, " +
|
||||
DATE_RECEIVED + " INTEGER, " +
|
||||
MESSAGE_BOX + " INTEGER, " +
|
||||
READ + " INTEGER DEFAULT 0, " +
|
||||
"m_id" + " TEXT, " +
|
||||
"sub" + " TEXT, " +
|
||||
"sub_cs" + " INTEGER, " +
|
||||
BODY + " TEXT, " +
|
||||
PART_COUNT + " INTEGER, " +
|
||||
"ct_t" + " TEXT, " +
|
||||
CONTENT_LOCATION + " TEXT, " +
|
||||
RECIPIENT_ID + " INTEGER, " +
|
||||
ADDRESS_DEVICE_ID + " INTEGER, " +
|
||||
EXPIRY + " INTEGER, " +
|
||||
"m_cls" + " TEXT, " +
|
||||
MESSAGE_TYPE + " INTEGER, " +
|
||||
"v" + " INTEGER, " +
|
||||
MESSAGE_SIZE + " INTEGER, " +
|
||||
"pri" + " INTEGER, " +
|
||||
"rr" + " INTEGER, " +
|
||||
"rpt_a" + " INTEGER, " +
|
||||
"resp_st" + " INTEGER, " +
|
||||
STATUS + " INTEGER, " +
|
||||
TRANSACTION_ID + " TEXT, " +
|
||||
"retr_st" + " INTEGER, " +
|
||||
"retr_txt" + " TEXT, " +
|
||||
"retr_txt_cs" + " INTEGER, " +
|
||||
"read_status" + " INTEGER, " +
|
||||
"ct_cls" + " INTEGER, " +
|
||||
"resp_txt" + " TEXT, " +
|
||||
"d_tm" + " INTEGER, " +
|
||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
|
||||
NETWORK_FAILURE + " TEXT DEFAULT NULL," +
|
||||
"d_rpt" + " INTEGER, " +
|
||||
SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
|
||||
EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||
EXPIRE_STARTED + " INTEGER DEFAULT 0, " +
|
||||
NOTIFIED + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_ID + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_AUTHOR + " TEXT, " +
|
||||
QUOTE_BODY + " TEXT, " +
|
||||
QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
|
||||
QUOTE_MISSING + " INTEGER DEFAULT 0, " +
|
||||
SHARED_CONTACTS + " TEXT, " +
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
LINK_PREVIEWS + " TEXT, " +
|
||||
VIEW_ONCE + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS + " BLOB DEFAULT NULL, " +
|
||||
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
@ -152,7 +187,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
BODY, PART_COUNT, RECIPIENT_ID, ADDRESS_DEVICE_ID,
|
||||
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
||||
@ -334,6 +369,18 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException {
|
||||
try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) {
|
||||
MessageRecord record = new Reader(cursor).getNext();
|
||||
|
||||
if (record == null) {
|
||||
throw new NoSuchMessageException("No message for ID: " + messageId);
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
public Reader getExpireStartedMessages() {
|
||||
String where = EXPIRE_STARTED + " > 0";
|
||||
return readerFor(rawQuery(where, null));
|
||||
@ -1414,7 +1461,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
message.getOutgoingQuote().isOriginalMissing(),
|
||||
new SlideDeck(context, message.getOutgoingQuote().getAttachments())) :
|
||||
null,
|
||||
message.getSharedContacts(), message.getLinkPreviews(), false);
|
||||
message.getSharedContacts(), message.getLinkPreviews(), false, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1486,24 +1533,25 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
|
||||
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_SENT));
|
||||
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED));
|
||||
long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
|
||||
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID));
|
||||
int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID));
|
||||
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.DELIVERY_RECEIPT_COUNT));
|
||||
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.READ_RECEIPT_COUNT));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY));
|
||||
int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT));
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
|
||||
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRES_IN));
|
||||
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED));
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1;
|
||||
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1;
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID ));
|
||||
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_SENT ));
|
||||
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED));
|
||||
long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX ));
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID ));
|
||||
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID ));
|
||||
int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID ));
|
||||
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.DELIVERY_RECEIPT_COUNT));
|
||||
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.READ_RECEIPT_COUNT ));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY ));
|
||||
int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT ));
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
|
||||
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE ));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID ));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRES_IN ));
|
||||
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED ));
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1;
|
||||
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1;
|
||||
List<ReactionRecord> reactions = parseReactions(cursor);
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
readReceiptCount = 0;
|
||||
@ -1524,7 +1572,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount,
|
||||
threadId, body, slideDeck, partCount, box, mismatches,
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified);
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions);
|
||||
}
|
||||
|
||||
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {
|
||||
|
@ -20,6 +20,9 @@ public interface MmsSmsColumns {
|
||||
public static final String EXPIRE_STARTED = "expire_started";
|
||||
public static final String NOTIFIED = "notified";
|
||||
public static final String UNIDENTIFIED = "unidentified";
|
||||
public static final String REACTIONS = "reactions";
|
||||
public static final String REACTIONS_UNREAD = "reactions_unread";
|
||||
public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
|
||||
|
||||
public static class Types {
|
||||
protected static final long TOTAL_MASK = 0xFFFFFFFF;
|
||||
|
@ -52,6 +52,7 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
|
||||
SmsDatabase.STATUS,
|
||||
MmsSmsColumns.UNIDENTIFIED,
|
||||
MmsSmsColumns.REACTIONS,
|
||||
MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
|
||||
@ -73,7 +74,11 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE};
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
MmsSmsColumns.READ,
|
||||
MmsSmsColumns.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN};
|
||||
|
||||
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
@ -133,7 +138,7 @@ public class MmsSmsDatabase extends Database {
|
||||
|
||||
public Cursor getUnread() {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
|
||||
String selection = MmsSmsColumns.READ + " = 0 AND " + MmsSmsColumns.NOTIFIED + " = 0";
|
||||
String selection = MmsSmsColumns.NOTIFIED + " = 0 AND (" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + ")";
|
||||
|
||||
return queryTables(PROJECTION, selection, order, null);
|
||||
}
|
||||
@ -149,6 +154,18 @@ public class MmsSmsDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkMessageExists(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms()) {
|
||||
try (Cursor mms = DatabaseFactory.getMmsDatabase(context).getMessage(messageRecord.getId())) {
|
||||
return mms != null && mms.getCount() > 0;
|
||||
}
|
||||
} else {
|
||||
try (Cursor sms = DatabaseFactory.getSmsDatabase(context).getMessageCursor(messageRecord.getId())) {
|
||||
return sms != null && sms.getCount() > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getConversationCount(long threadId) {
|
||||
int count = DatabaseFactory.getSmsDatabase(context).getMessageCountForThread(threadId);
|
||||
count += DatabaseFactory.getMmsDatabase(context).getMessageCountForThread(threadId);
|
||||
@ -299,7 +316,10 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE};
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
MmsDatabase.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN};
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
@ -326,7 +346,10 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE};
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
MmsDatabase.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN};
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
|
||||
@ -398,6 +421,9 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS);
|
||||
mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS);
|
||||
mmsColumnsPresent.add(MmsDatabase.VIEW_ONCE);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
@ -419,6 +445,9 @@ public class MmsSmsDatabase extends Database {
|
||||
smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED);
|
||||
smsColumnsPresent.add(SmsDatabase.STATUS);
|
||||
smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN);
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, null);
|
||||
|
@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
@ -49,6 +50,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -75,14 +77,32 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
public static final String SUBJECT = "subject";
|
||||
public static final String SERVICE_CENTER = "service_center";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " +
|
||||
THREAD_ID + " INTEGER, " + RECIPIENT_ID + " INTEGER, " + ADDRESS_DEVICE_ID + " INTEGER DEFAULT 1, " + PERSON + " INTEGER, " +
|
||||
DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
|
||||
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
|
||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " +
|
||||
MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
|
||||
EXPIRES_IN + " INTEGER DEFAULT 0, " + EXPIRE_STARTED + " INTEGER DEFAULT 0, " + NOTIFIED + " DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNIDENTIFIED + " INTEGER DEFAULT 0);";
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
THREAD_ID + " INTEGER, " +
|
||||
RECIPIENT_ID + " INTEGER, " +
|
||||
ADDRESS_DEVICE_ID + " INTEGER DEFAULT 1, " +
|
||||
PERSON + " INTEGER, " +
|
||||
DATE_RECEIVED + " INTEGER, " +
|
||||
DATE_SENT + " INTEGER, " +
|
||||
PROTOCOL + " INTEGER, " +
|
||||
READ + " INTEGER DEFAULT 0, " +
|
||||
STATUS + " INTEGER DEFAULT -1," +
|
||||
TYPE + " INTEGER, " +
|
||||
REPLY_PATH_PRESENT + " INTEGER, " +
|
||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0," +
|
||||
SUBJECT + " TEXT, " +
|
||||
BODY + " TEXT, " +
|
||||
MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
|
||||
SERVICE_CENTER + " TEXT, " +
|
||||
SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
|
||||
EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||
EXPIRE_STARTED + " INTEGER DEFAULT 0, " +
|
||||
NOTIFIED + " DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS + " BLOB DEFAULT NULL, " +
|
||||
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
@ -100,7 +120,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
PROTOCOL, READ, STATUS, TYPE,
|
||||
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT,
|
||||
MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED,
|
||||
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED
|
||||
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN
|
||||
};
|
||||
|
||||
private final String OUTGOING_INSECURE_MESSAGE_CLAUSE = "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + TYPE + " & " + Types.SECURE_MESSAGE_BIT + ")";
|
||||
@ -868,7 +888,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(),
|
||||
threadId, 0, new LinkedList<IdentityKeyMismatch>(),
|
||||
message.getSubscriptionId(), message.getExpiresIn(),
|
||||
System.currentTimeMillis(), 0, false);
|
||||
System.currentTimeMillis(), 0, false, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
@ -893,22 +913,23 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
public SmsMessageRecord getCurrent() {
|
||||
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
|
||||
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.RECIPIENT_ID));
|
||||
int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS_DEVICE_ID));
|
||||
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
|
||||
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_RECEIVED));
|
||||
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_SENT));
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
|
||||
int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.STATUS));
|
||||
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.DELIVERY_RECEIPT_COUNT));
|
||||
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.READ_RECEIPT_COUNT));
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.SUBSCRIPTION_ID));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRES_IN));
|
||||
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1;
|
||||
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID ));
|
||||
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.RECIPIENT_ID ));
|
||||
int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS_DEVICE_ID ));
|
||||
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE ));
|
||||
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_RECEIVED));
|
||||
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_SENT ));
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID ));
|
||||
int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.STATUS ));
|
||||
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.DELIVERY_RECEIPT_COUNT));
|
||||
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.READ_RECEIPT_COUNT ));
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.SUBSCRIPTION_ID ));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRES_IN ));
|
||||
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED ));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY ));
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1;
|
||||
List<ReactionRecord> reactions = parseReactions(cursor);
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
readReceiptCount = 0;
|
||||
@ -923,7 +944,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
dateSent, dateReceived, deliveryReceiptCount, type,
|
||||
threadId, status, mismatches, subscriptionId,
|
||||
expiresIn, expireStarted,
|
||||
readReceiptCount, unidentified);
|
||||
readReceiptCount, unidentified, reactions);
|
||||
}
|
||||
|
||||
private List<IdentityKeyMismatch> getMismatches(String document) {
|
||||
|
@ -278,6 +278,9 @@ public class ThreadDatabase extends Database {
|
||||
final List<MarkedMessageInfo> smsRecords = DatabaseFactory.getSmsDatabase(context).setAllMessagesRead();
|
||||
final List<MarkedMessageInfo> mmsRecords = DatabaseFactory.getMmsDatabase(context).setAllMessagesRead();
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).setAllReactionsSeen();
|
||||
DatabaseFactory.getMmsDatabase(context).setAllReactionsSeen();
|
||||
|
||||
notifyConversationListListeners();
|
||||
|
||||
return Util.concatenatedList(smsRecords, mmsRecords);
|
||||
@ -312,6 +315,9 @@ public class ThreadDatabase extends Database {
|
||||
final List<MarkedMessageInfo> smsRecords = DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId);
|
||||
final List<MarkedMessageInfo> mmsRecords = DatabaseFactory.getMmsDatabase(context).setMessagesRead(threadId);
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).setReactionsSeen(threadId);
|
||||
DatabaseFactory.getMmsDatabase(context).setReactionsSeen(threadId);
|
||||
|
||||
notifyConversationListListeners();
|
||||
|
||||
return Util.concatenatedList(smsRecords, mmsRecords);
|
||||
|
@ -51,7 +51,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@ -93,8 +92,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int ATTACHMENT_CLEAR_HASHES_2 = 34;
|
||||
private static final int UUIDS = 35;
|
||||
private static final int USERNAMES = 36;
|
||||
private static final int REACTIONS = 37;
|
||||
|
||||
private static final int DATABASE_VERSION = 37;
|
||||
|
||||
private static final int DATABASE_VERSION = 36;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -626,6 +627,17 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS recipient_username_index ON recipient (username)");
|
||||
}
|
||||
|
||||
if (oldVersion < REACTIONS) {
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN reactions BLOB DEFAULT NULL");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN reactions BLOB DEFAULT NULL");
|
||||
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN reactions_unread INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN reactions_unread INTEGER DEFAULT 0");
|
||||
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN reactions_last_seen INTEGER DEFAULT -1");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN reactions_last_seen INTEGER DEFAULT -1");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -57,12 +57,13 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
long expiresIn, long expireStarted,
|
||||
boolean viewOnce, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
|
||||
readReceiptCount, quote, contacts, linkPreviews, unidentified);
|
||||
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions);
|
||||
this.partCount = partCount;
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
private final long expiresIn;
|
||||
private final long expireStarted;
|
||||
private final boolean unidentified;
|
||||
private final List<ReactionRecord> reactions;
|
||||
|
||||
MessageRecord(long id, String body, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId,
|
||||
@ -61,7 +62,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures,
|
||||
int subscriptionId, long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified)
|
||||
int readReceiptCount, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
{
|
||||
super(body, conversationRecipient, dateSent, dateReceived,
|
||||
threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount);
|
||||
@ -74,6 +76,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStarted = expireStarted;
|
||||
this.unidentified = unidentified;
|
||||
this.reactions = reactions;
|
||||
}
|
||||
|
||||
public abstract boolean isMms();
|
||||
@ -249,4 +252,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
public boolean isViewOnce() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull List<ReactionRecord> getReactions() {
|
||||
return reactions;
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,10 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||
long expireStarted, boolean viewOnce,
|
||||
@NonNull SlideDeck slideDeck, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, reactions);
|
||||
|
||||
this.slideDeck = slideDeck;
|
||||
this.quote = quote;
|
||||
|
@ -57,7 +57,8 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
super(id, "", conversationRecipient, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId,
|
||||
0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false);
|
||||
0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false,
|
||||
Collections.emptyList());
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
|
@ -0,0 +1,39 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
public class ReactionRecord {
|
||||
private final String emoji;
|
||||
private final RecipientId author;
|
||||
private final long dateSent;
|
||||
private final long dateReceived;
|
||||
|
||||
public ReactionRecord(@NonNull String emoji,
|
||||
@NonNull RecipientId author,
|
||||
long dateSent,
|
||||
long dateReceived)
|
||||
{
|
||||
this.emoji = emoji;
|
||||
this.author = author;
|
||||
this.dateSent = dateSent;
|
||||
this.dateReceived = dateReceived;
|
||||
}
|
||||
|
||||
public @NonNull String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public long getDateSent() {
|
||||
return dateSent;
|
||||
}
|
||||
|
||||
public long getDateReceived() {
|
||||
return dateReceived;
|
||||
}
|
||||
}
|
@ -48,12 +48,13 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
long type, long threadId,
|
||||
int status, List<IdentityKeyMismatch> mismatches,
|
||||
int subscriptionId, long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified)
|
||||
int readReceiptCount, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
{
|
||||
super(id, body, recipient, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, status, deliveryReceiptCount, type,
|
||||
mismatches, new LinkedList<>(), subscriptionId,
|
||||
expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
expiresIn, expireStarted, readReceiptCount, unidentified, reactions);
|
||||
}
|
||||
|
||||
public long getType() {
|
||||
|
@ -66,6 +66,7 @@ public final class JobManagerFactories {
|
||||
put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory());
|
||||
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
|
||||
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
|
||||
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
|
||||
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
||||
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
|
||||
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
||||
|
@ -59,6 +59,7 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
|
||||
@ -241,12 +242,13 @@ public class PushDecryptJob extends BaseJob {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
|
||||
|
||||
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), message.getGroupInfo(), content.getTimestamp(), smsMessageId);
|
||||
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
|
||||
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId);
|
||||
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), message.getGroupInfo(), content.getTimestamp(), smsMessageId);
|
||||
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
|
||||
else if (message.getReaction().isPresent()) handleReaction(content, message);
|
||||
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId);
|
||||
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
|
||||
handleUnknownGroupMessage(content, message.getGroupInfo().get());
|
||||
@ -525,6 +527,30 @@ public class PushDecryptJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
||||
SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
|
||||
|
||||
Recipient targetAuthor = Recipient.externalPush(context, reaction.getTargetAuthor());
|
||||
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId());
|
||||
|
||||
if (targetMessage != null) {
|
||||
Recipient reactionAuthor = Recipient.externalPush(context, content.getSender());
|
||||
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
if (reaction.isRemove()) {
|
||||
db.deleteReaction(targetMessage.getId(), reactionAuthor.getId());
|
||||
MessageNotifier.updateNotification(context);
|
||||
} else {
|
||||
ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), reactionAuthor.getId(), message.getTimestamp(), System.currentTimeMillis());
|
||||
db.addReaction(targetMessage.getId(), reactionRecord);
|
||||
MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false);
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.w(TAG, "[handleReaction] Could not find matching message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) {
|
||||
IdentityUtil.processVerifiedMessage(context, verifiedMessage);
|
||||
}
|
||||
@ -599,6 +625,10 @@ public class PushDecryptJob extends BaseJob {
|
||||
threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true);
|
||||
} else if (message.getMessage().isExpirationUpdate()) {
|
||||
threadId = handleSynchronizeSentExpirationUpdate(message);
|
||||
} else if (message.getMessage().getReaction().isPresent()) {
|
||||
handleReaction(content, message.getMessage());
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
|
||||
threadId = threadId != -1 ? threadId : null;
|
||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent()) {
|
||||
threadId = handleSynchronizeSentMediaMessage(message);
|
||||
} else {
|
||||
|
283
src/org/thoughtcrime/securesms/jobs/ReactionSendJob.java
Normal file
@ -0,0 +1,283 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ReactionSendJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "ReactionSendJob";
|
||||
|
||||
private static final String TAG = Log.tag(ReactionSendJob.class);
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_IS_MMS = "is_mms";
|
||||
private static final String KEY_REACTION_EMOJI = "reaction_emoji";
|
||||
private static final String KEY_REACTION_AUTHOR = "reaction_author";
|
||||
private static final String KEY_REACTION_DATE_SENT = "reaction_date_sent";
|
||||
private static final String KEY_REACTION_DATE_RECEIVED = "reaction_date_received";
|
||||
private static final String KEY_REMOVE = "remove";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count";
|
||||
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
private final List<RecipientId> recipients;
|
||||
private final int initialRecipientCount;
|
||||
private final ReactionRecord reaction;
|
||||
private final boolean remove;
|
||||
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull ReactionSendJob create(@NonNull Context context,
|
||||
long messageId,
|
||||
boolean isMms,
|
||||
@NonNull ReactionRecord reaction,
|
||||
boolean remove)
|
||||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
if (conversationRecipient == null) {
|
||||
throw new AssertionError("We have a message, but couldn't find the thread!");
|
||||
}
|
||||
|
||||
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
|
||||
: Arrays.asList(conversationRecipient.getId());
|
||||
|
||||
recipients.remove(Recipient.self().getId());
|
||||
|
||||
return new ReactionSendJob(messageId,
|
||||
isMms,
|
||||
recipients,
|
||||
recipients.size(),
|
||||
reaction,
|
||||
remove,
|
||||
new Parameters.Builder()
|
||||
.setQueue(conversationRecipient.getId().toQueueKey())
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build());
|
||||
}
|
||||
|
||||
private ReactionSendJob(long messageId,
|
||||
boolean isMms,
|
||||
@NonNull List<RecipientId> recipients,
|
||||
int initialRecipientCount,
|
||||
@NonNull ReactionRecord reaction,
|
||||
boolean remove,
|
||||
@NonNull Parameters parameters)
|
||||
{
|
||||
super(parameters);
|
||||
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.recipients = recipients;
|
||||
this.initialRecipientCount = initialRecipientCount;
|
||||
this.reaction = reaction;
|
||||
this.remove = remove;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putBoolean(KEY_IS_MMS, isMms)
|
||||
.putString(KEY_REACTION_EMOJI, reaction.getEmoji())
|
||||
.putString(KEY_REACTION_AUTHOR, reaction.getAuthor().serialize())
|
||||
.putLong(KEY_REACTION_DATE_SENT, reaction.getDateSent())
|
||||
.putLong(KEY_REACTION_DATE_RECEIVED, reaction.getDateReceived())
|
||||
.putBoolean(KEY_REMOVE, remove)
|
||||
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
|
||||
.putInt(KEY_INITIAL_RECIPIENT_COUNT, initialRecipientCount)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
MessagingDatabase db;
|
||||
MessageRecord message;
|
||||
|
||||
if (isMms) {
|
||||
db = DatabaseFactory.getMmsDatabase(context);
|
||||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} else {
|
||||
db = DatabaseFactory.getSmsDatabase(context);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
}
|
||||
|
||||
Recipient targetAuthor = message.isOutgoing() ? Recipient.self() : message.getIndividualRecipient();
|
||||
long targetSentTimestamp = message.getDateSent();
|
||||
|
||||
if (!remove && !db.hasReaction(messageId, reaction)) {
|
||||
Log.w(TAG, "Went to add a reaction, but it's no longer present on the message!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (remove && db.hasReaction(messageId, reaction)) {
|
||||
Log.w(TAG, "Went to remove a reaction, but it's still there!");
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
if (conversationRecipient == null) {
|
||||
throw new AssertionError("We have a message, but couldn't find the thread!");
|
||||
}
|
||||
|
||||
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
|
||||
List<Recipient> completions = deliver(conversationRecipient, destinations, targetAuthor, targetSentTimestamp);
|
||||
|
||||
for (Recipient completion : completions) {
|
||||
recipients.remove(completion.getId());
|
||||
}
|
||||
|
||||
Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size());
|
||||
|
||||
if (!recipients.isEmpty()) {
|
||||
Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof IOException ||
|
||||
e instanceof RetryLaterException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
if (recipients.size() < initialRecipientCount) {
|
||||
Log.w(TAG, "Only sent a reaction to " + recipients.size() + "/" + initialRecipientCount + " recipients. Still, it sent to someone, so it stays.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "Failed to send the reaction to all recipients!");
|
||||
|
||||
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
if (remove && !db.hasReaction(messageId, reaction)) {
|
||||
Log.w(TAG, "Reaction removal failed, so adding the reaction back.");
|
||||
db.addReaction(messageId, reaction);
|
||||
} else if (!remove && db.hasReaction(messageId, reaction)){
|
||||
Log.w(TAG, "Reaction addition failed, so removing the reaction.");
|
||||
db.deleteReaction(messageId, reaction.getAuthor());
|
||||
} else {
|
||||
Log.w(TAG, "Reaction state didn't match what we'd expect to revert it, so we're just leaving it alone.");
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, @NonNull Recipient targetAuthor, long targetSentTimestamp)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList();
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp));
|
||||
|
||||
if (conversationRecipient.isGroup()) {
|
||||
dataMessage.asGroupMessage(new SignalServiceGroup(GroupUtil.getDecodedId(conversationRecipient.requireGroupId())));
|
||||
}
|
||||
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
|
||||
Stream.of(results)
|
||||
.filter(r -> r.getIdentityFailure() != null)
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.forEach(r -> Log.w(TAG, "Identity failure for " + r.getId()));
|
||||
|
||||
Stream.of(results)
|
||||
.filter(SendMessageResult::isUnregisteredFailure)
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.forEach(r -> Log.w(TAG, "Unregistered failure for " + r.getId()));
|
||||
|
||||
|
||||
return Stream.of(results)
|
||||
.filter(r -> r.getSuccess() != null || r.getIdentityFailure() != null || r.isUnregisteredFailure())
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.Reaction buildReaction(@NonNull Context context,
|
||||
@NonNull ReactionRecord reaction,
|
||||
boolean remove,
|
||||
@NonNull Recipient targetAuthor,
|
||||
long targetSentTimestamp)
|
||||
{
|
||||
return new SignalServiceDataMessage.Reaction(reaction.getEmoji(),
|
||||
remove,
|
||||
RecipientUtil.toSignalServiceAddress(context, targetAuthor),
|
||||
targetSentTimestamp);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<ReactionSendJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
ReactionSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
long messageId = data.getLong(KEY_MESSAGE_ID);
|
||||
boolean isMms = data.getBoolean(KEY_IS_MMS);
|
||||
List<RecipientId> recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS));
|
||||
int initialRecipientCount = data.getInt(KEY_INITIAL_RECIPIENT_COUNT);
|
||||
ReactionRecord reaction = new ReactionRecord(data.getString(KEY_REACTION_EMOJI),
|
||||
RecipientId.from(data.getString(KEY_REACTION_AUTHOR)),
|
||||
data.getLong(KEY_REACTION_DATE_SENT),
|
||||
data.getLong(KEY_REACTION_DATE_RECEIVED));
|
||||
boolean remove = data.getBoolean(KEY_REMOVE);
|
||||
|
||||
return new ReactionSendJob(messageId, isMms, recipients, initialRecipientCount, reaction, remove, parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -37,6 +37,9 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
@ -46,11 +49,13 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
@ -66,9 +71,11 @@ import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -90,6 +97,7 @@ public class MessageNotifier {
|
||||
|
||||
public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply";
|
||||
|
||||
private static final String EMOJI_REPLACEMENT_STRING = "__EMOJI__";
|
||||
private static final int SUMMARY_NOTIFICATION_ID = 1338;
|
||||
private static final int PENDING_MESSAGES_ID = 1111;
|
||||
private static final String NOTIFICATION_GROUP = "messages";
|
||||
@ -455,34 +463,73 @@ public class MessageNotifier {
|
||||
Recipient threadRecipients = null;
|
||||
SlideDeck slideDeck = null;
|
||||
long timestamp = record.getTimestamp();
|
||||
|
||||
boolean isUnreadMessage = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.READ)) == 0;
|
||||
boolean hasUnreadReactions = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_UNREAD)) == 1;
|
||||
long lastReactionRead = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_LAST_SEEN));
|
||||
|
||||
if (threadId != -1) {
|
||||
threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
}
|
||||
|
||||
if (KeyCachingService.isLocked(context)) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
|
||||
} else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
|
||||
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
|
||||
body = ContactUtil.getStringSummary(context, contact);
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker));
|
||||
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
|
||||
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record)));
|
||||
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
|
||||
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
||||
} else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||
String message = context.getString(R.string.MessageNotifier_media_message_with_text, body);
|
||||
int italicLength = message.length() - body.length();
|
||||
body = SpanUtil.italic(message, italicLength);
|
||||
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
||||
if (isUnreadMessage) {
|
||||
if (KeyCachingService.isLocked(context)) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
|
||||
} else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
|
||||
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
|
||||
body = ContactUtil.getStringSummary(context, contact);
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker));
|
||||
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
|
||||
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record)));
|
||||
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
|
||||
slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck();
|
||||
} else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||
String message = context.getString(R.string.MessageNotifier_media_message_with_text, body);
|
||||
int italicLength = message.length() - body.length();
|
||||
body = SpanUtil.italic(message, italicLength);
|
||||
slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck();
|
||||
}
|
||||
|
||||
if (threadRecipients == null || !threadRecipients.isMuted()) {
|
||||
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
|
||||
}
|
||||
}
|
||||
|
||||
if (threadRecipients == null || !threadRecipients.isMuted()) {
|
||||
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
|
||||
if (hasUnreadReactions) {
|
||||
for (ReactionRecord reaction : record.getReactions()) {
|
||||
Recipient reactionSender = Recipient.resolved(reaction.getAuthor());
|
||||
if (reactionSender.equals(Recipient.self()) || !record.isOutgoing() || reaction.getDateReceived() <= lastReactionRead) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (KeyCachingService.isLocked(context)) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
|
||||
} else {
|
||||
String text = SpanUtil.italic(context.getString(R.string.MessageNotifier_reacted_to_your_message, EMOJI_REPLACEMENT_STRING)).toString();
|
||||
String[] parts = text.split(EMOJI_REPLACEMENT_STRING);
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
builder.append(SpanUtil.italic(parts[i]));
|
||||
|
||||
if (i != parts.length -1) {
|
||||
builder.append(reaction.getEmoji());
|
||||
}
|
||||
}
|
||||
|
||||
if (text.endsWith(EMOJI_REPLACEMENT_STRING)) {
|
||||
builder.append(reaction.getEmoji());
|
||||
}
|
||||
|
||||
body = builder;
|
||||
}
|
||||
|
||||
if (threadRecipients == null || !threadRecipients.isMuted()) {
|
||||
notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@ -13,6 +14,9 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -21,10 +25,9 @@ public class NotificationState {
|
||||
|
||||
private static final String TAG = NotificationState.class.getSimpleName();
|
||||
|
||||
private final LinkedList<NotificationItem> notifications = new LinkedList<>();
|
||||
private final LinkedHashSet<Long> threads = new LinkedHashSet<>();
|
||||
|
||||
private int notificationCount = 0;
|
||||
private final Comparator<NotificationItem> notificationItemComparator = (a, b) -> -Long.compare(a.getTimestamp(), b.getTimestamp());
|
||||
private final List<NotificationItem> notifications = new LinkedList<>();
|
||||
private final LinkedHashSet<Long> threads = new LinkedHashSet<>();
|
||||
|
||||
public NotificationState() {}
|
||||
|
||||
@ -35,19 +38,16 @@ public class NotificationState {
|
||||
}
|
||||
|
||||
public void addNotification(NotificationItem item) {
|
||||
notifications.addFirst(item);
|
||||
|
||||
if (threads.contains(item.getThreadId())) {
|
||||
threads.remove(item.getThreadId());
|
||||
}
|
||||
notifications.add(item);
|
||||
Collections.sort(notifications, notificationItemComparator);
|
||||
|
||||
threads.remove(item.getThreadId());
|
||||
threads.add(item.getThreadId());
|
||||
notificationCount++;
|
||||
}
|
||||
|
||||
public @Nullable Uri getRingtone(@NonNull Context context) {
|
||||
if (!notifications.isEmpty()) {
|
||||
Recipient recipient = notifications.getFirst().getRecipient();
|
||||
Recipient recipient = notifications.get(0).getRecipient();
|
||||
|
||||
if (recipient != null) {
|
||||
return NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient)
|
||||
@ -60,7 +60,7 @@ public class NotificationState {
|
||||
|
||||
public VibrateState getVibrate() {
|
||||
if (!notifications.isEmpty()) {
|
||||
Recipient recipient = notifications.getFirst().getRecipient();
|
||||
Recipient recipient = notifications.get(0).getRecipient();
|
||||
|
||||
if (recipient != null) {
|
||||
return recipient.resolve().getMessageVibrate();
|
||||
@ -74,7 +74,7 @@ public class NotificationState {
|
||||
return threads.size() > 1;
|
||||
}
|
||||
|
||||
public LinkedHashSet<Long> getThreads() {
|
||||
public Collection<Long> getThreads() {
|
||||
return threads;
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ public class NotificationState {
|
||||
}
|
||||
|
||||
public int getMessageCount() {
|
||||
return notificationCount;
|
||||
return notifications.size();
|
||||
}
|
||||
|
||||
public List<NotificationItem> getNotifications() {
|
||||
@ -91,12 +91,13 @@ public class NotificationState {
|
||||
}
|
||||
|
||||
public List<NotificationItem> getNotificationsForThread(long threadId) {
|
||||
LinkedList<NotificationItem> list = new LinkedList<>();
|
||||
List<NotificationItem> list = new LinkedList<>();
|
||||
|
||||
for (NotificationItem item : notifications) {
|
||||
if (item.getThreadId() == threadId) list.addFirst(item);
|
||||
if (item.getThreadId() == threadId) list.add(item);
|
||||
}
|
||||
|
||||
Collections.sort(list, notificationItemComparator);
|
||||
return list;
|
||||
}
|
||||
|
||||
|
21
src/org/thoughtcrime/securesms/reactions/EmojiCount.java
Normal file
@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
final class EmojiCount {
|
||||
private final String emoji;
|
||||
private final int count;
|
||||
|
||||
EmojiCount(@NonNull String emoji, int count) {
|
||||
this.emoji = emoji;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public @NonNull String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmojiCountAdapter.ViewHolder> {
|
||||
|
||||
private List<EmojiCount> emojiCountList = Collections.emptyList();
|
||||
private int selectedPosition = -1;
|
||||
|
||||
private final OnEmojiCountSelectedListener onEmojiCountSelectedListener;
|
||||
|
||||
ReactionEmojiCountAdapter(@NonNull OnEmojiCountSelectedListener onEmojiCountSelectedListener) {
|
||||
this.onEmojiCountSelectedListener = onEmojiCountSelectedListener;
|
||||
}
|
||||
|
||||
void updateData(@NonNull List<EmojiCount> newEmojiCount) {
|
||||
if (selectedPosition != -1) {
|
||||
EmojiCount oldSelection = emojiCountList.get(selectedPosition);
|
||||
int newPosition = -1;
|
||||
|
||||
for (int i = 0; i < newEmojiCount.size(); i++) {
|
||||
if (newEmojiCount.get(i).getEmoji().equals(oldSelection.getEmoji())) {
|
||||
newPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPosition == -1 && !newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(newEmojiCount.get(0));
|
||||
} else {
|
||||
selectedPosition = newPosition;
|
||||
}
|
||||
} else if (!newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(newEmojiCount.get(0));
|
||||
}
|
||||
|
||||
this.emojiCountList = newEmojiCount;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_emoji_item, parent, false), position -> {
|
||||
if (position != -1 && position != selectedPosition) {
|
||||
onEmojiCountSelectedListener.onSelected(emojiCountList.get(position));
|
||||
|
||||
int oldPosition = selectedPosition;
|
||||
selectedPosition = position;
|
||||
|
||||
notifyItemChanged(oldPosition);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(emojiCountList.get(position), selectedPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return emojiCountList.size();
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final Drawable selected;
|
||||
private final EmojiTextView emojiView;
|
||||
private final TextView countView;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull OnViewHolderClickListener onClickListener) {
|
||||
super(itemView);
|
||||
emojiView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
|
||||
countView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_text);
|
||||
selected = ThemeUtil.getThemedDrawable(itemView.getContext(), R.attr.reactions_bottom_dialog_fragment_emoji_selected);
|
||||
|
||||
itemView.setOnClickListener(v -> onClickListener.onClick(getAdapterPosition()));
|
||||
}
|
||||
|
||||
void bind(@NonNull EmojiCount emojiCount, int selectedPosition) {
|
||||
emojiView.setText(emojiCount.getEmoji());
|
||||
countView.setText(String.valueOf(emojiCount.getCount()));
|
||||
itemView.setBackground(getAdapterPosition() == selectedPosition ? selected : null);
|
||||
}
|
||||
}
|
||||
|
||||
interface OnViewHolderClickListener {
|
||||
void onClick(int position);
|
||||
}
|
||||
|
||||
interface OnEmojiCountSelectedListener {
|
||||
void onSelected(@NonNull EmojiCount emojiCount);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecipientsAdapter.ViewHolder> {
|
||||
|
||||
private List<Recipient> data = Collections.emptyList();
|
||||
|
||||
public void updateData(List<Recipient> newData) {
|
||||
data = newData;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recipient_item,
|
||||
parent,
|
||||
false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView recipient;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
avatar = itemView.findViewById(R.id.reactions_bottom_view_recipient_avatar);
|
||||
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
|
||||
}
|
||||
|
||||
void bind(Recipient recipient) {
|
||||
this.recipient.setText(recipient.getDisplayName(itemView.getContext()));
|
||||
|
||||
if (recipient.equals(Recipient.self())) {
|
||||
AvatarUtil.loadIconIntoImageView(recipient, avatar);
|
||||
} else {
|
||||
this.avatar.setAvatar(GlideApp.with(avatar), recipient, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogFragment {
|
||||
|
||||
private static final String ARGS_MESSAGE_ID = "reactions.args.message.id";
|
||||
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
|
||||
|
||||
private long messageId;
|
||||
private RecyclerView recipientRecyclerView;
|
||||
private RecyclerView emojiRecyclerView;
|
||||
private ReactionsLoader reactionsLoader;
|
||||
private ReactionRecipientsAdapter recipientsAdapter;
|
||||
private ReactionEmojiCountAdapter emojiCountAdapter;
|
||||
private ReactionsViewModel viewModel;
|
||||
|
||||
public static DialogFragment create(long messageId, boolean isMms) {
|
||||
Bundle args = new Bundle();
|
||||
DialogFragment fragment = new ReactionsBottomSheetDialogFragment();
|
||||
|
||||
args.putLong(ARGS_MESSAGE_ID, messageId);
|
||||
args.putBoolean(ARGS_IS_MMS, isMms);
|
||||
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.reactions_bottom_sheet_dialog_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
recipientRecyclerView = view.findViewById(R.id.reactions_bottom_view_recipient_recycler);
|
||||
emojiRecyclerView = view.findViewById(R.id.reactions_bottom_view_emoji_recycler);
|
||||
|
||||
messageId = getArguments().getLong(ARGS_MESSAGE_ID);
|
||||
|
||||
setUpRecipientsRecyclerView();
|
||||
setUpEmojiRecyclerView();
|
||||
setUpViewModel();
|
||||
|
||||
LoaderManager.getInstance(requireActivity()).initLoader((int) messageId, null, reactionsLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
LoaderManager.getInstance(requireActivity()).destroyLoader((int) messageId);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void setUpRecipientsRecyclerView() {
|
||||
recipientsAdapter = new ReactionRecipientsAdapter();
|
||||
recipientRecyclerView.setAdapter(recipientsAdapter);
|
||||
}
|
||||
|
||||
private void setUpEmojiRecyclerView() {
|
||||
emojiCountAdapter = new ReactionEmojiCountAdapter((emojiCount -> viewModel.setFilterEmoji(emojiCount.getEmoji())));
|
||||
emojiRecyclerView.setAdapter(emojiCountAdapter);
|
||||
}
|
||||
|
||||
private void setUpViewModel() {
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
getArguments().getLong(ARGS_MESSAGE_ID),
|
||||
getArguments().getBoolean(ARGS_IS_MMS));
|
||||
|
||||
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(reactionsLoader);
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReactionsViewModel.class);
|
||||
|
||||
viewModel.getRecipients().observe(getViewLifecycleOwner(), reactions -> {
|
||||
if (reactions.size() == 0) dismiss();
|
||||
|
||||
recipientsAdapter.updateData(reactions);
|
||||
});
|
||||
|
||||
viewModel.getEmojiCounts().observe(getViewLifecycleOwner(), emojiCounts -> {
|
||||
if (emojiCounts.size() == 0) dismiss();
|
||||
|
||||
emojiCountAdapter.updateData(emojiCounts);
|
||||
});
|
||||
}
|
||||
}
|
130
src/org/thoughtcrime/securesms/reactions/ReactionsLoader.java
Normal file
@ -0,0 +1,130 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
private final Context appContext;
|
||||
|
||||
private MutableLiveData<List<Reaction>> internalLiveData = new MutableLiveData<>();
|
||||
|
||||
public ReactionsLoader(@NonNull Context context, long messageId, boolean isMms)
|
||||
{
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.appContext = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
return isMms ? new MmsMessageRecordCursorLoader(appContext, messageId)
|
||||
: new SmsMessageRecordCursorLoader(appContext, messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
MessageRecord record = isMms ? DatabaseFactory.getMmsDatabase(appContext).readerFor(data).getNext()
|
||||
: DatabaseFactory.getSmsDatabase(appContext).readerFor(data).getNext();
|
||||
|
||||
if (record == null) {
|
||||
internalLiveData.postValue(Collections.emptyList());
|
||||
} else {
|
||||
internalLiveData.postValue(Stream.of(record.getReactions())
|
||||
.map(reactionRecord -> new Reaction(Recipient.resolved(reactionRecord.getAuthor()),
|
||||
reactionRecord.getEmoji(),
|
||||
reactionRecord.getDateReceived()))
|
||||
.toList());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
// Do nothing?
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<List<Reaction>> getReactions() {
|
||||
return internalLiveData;
|
||||
}
|
||||
|
||||
private static final class MmsMessageRecordCursorLoader extends AbstractCursorLoader {
|
||||
|
||||
private final long messageId;
|
||||
|
||||
public MmsMessageRecordCursorLoader(@NonNull Context context, long messageId) {
|
||||
super(context);
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessage(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SmsMessageRecordCursorLoader extends AbstractCursorLoader {
|
||||
|
||||
private final long messageId;
|
||||
|
||||
public SmsMessageRecordCursorLoader(@NonNull Context context, long messageId) {
|
||||
super(context);
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessageCursor(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
static class Reaction {
|
||||
private final Recipient sender;
|
||||
private final String emoji;
|
||||
private final long timestamp;
|
||||
|
||||
private Reaction(@NonNull Recipient sender, @NonNull String emoji, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.emoji = emoji;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public @NonNull String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.thoughtcrime.securesms.reactions.ReactionsLoader.*;
|
||||
|
||||
public class ReactionsViewModel extends ViewModel {
|
||||
|
||||
private final Repository repository;
|
||||
private final MutableLiveData<String> filterEmoji = new MutableLiveData<>();
|
||||
|
||||
public ReactionsViewModel(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<Recipient>> getRecipients() {
|
||||
return Transformations.switchMap(filterEmoji,
|
||||
emoji -> Transformations.map(repository.getReactions(),
|
||||
reactions -> Stream.of(reactions)
|
||||
.filter(reaction -> reaction.getEmoji().equals(emoji))
|
||||
.map(Reaction::getSender).toList()));
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<EmojiCount>> getEmojiCounts() {
|
||||
return Transformations.map(repository.getReactions(),
|
||||
reactionList -> Stream.of(reactionList)
|
||||
.groupBy(Reaction::getEmoji)
|
||||
.sorted(this::compareReactions)
|
||||
.map(entry -> new EmojiCount(entry.getKey(), entry.getValue().size()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
public void setFilterEmoji(String filterEmoji) {
|
||||
this.filterEmoji.setValue(filterEmoji);
|
||||
}
|
||||
|
||||
private int compareReactions(@NonNull Map.Entry<String, List<Reaction>> lhs, @NonNull Map.Entry<String, List<Reaction>> rhs) {
|
||||
int lengthComparison = -Integer.compare(lhs.getValue().size(), rhs.getValue().size());
|
||||
if (lengthComparison != 0) return lengthComparison;
|
||||
|
||||
long latestTimestampLhs = getLatestTimestamp(lhs.getValue());
|
||||
long latestTimestampRhs = getLatestTimestamp(rhs.getValue());
|
||||
|
||||
return -Long.compare(latestTimestampLhs, latestTimestampRhs);
|
||||
}
|
||||
|
||||
private long getLatestTimestamp(List<Reaction> reactions) {
|
||||
return Stream.of(reactions)
|
||||
.max((a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp()))
|
||||
.map(Reaction::getTimestamp)
|
||||
.orElse(-1L);
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
LiveData<List<Reaction>> getReactions();
|
||||
}
|
||||
|
||||
static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Repository repository;
|
||||
|
||||
Factory(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return (T) new ReactionsViewModel(repository);
|
||||
}
|
||||
}
|
||||
}
|
@ -70,6 +70,10 @@ public class RecipientId implements Parcelable, Comparable<RecipientId> {
|
||||
return String.valueOf(id);
|
||||
}
|
||||
|
||||
public long toLong() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public @NonNull String toQueueKey() {
|
||||
return "RecipientId::" + id;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.telephony.SmsMessage;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
@ -37,6 +38,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
@ -48,22 +50,21 @@ import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@ -229,6 +230,32 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void sendNewReaction(@NonNull Context context, long messageId, boolean isMms, @NonNull String emoji) {
|
||||
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
ReactionRecord reaction = new ReactionRecord(emoji, Recipient.self().getId(), System.currentTimeMillis(), System.currentTimeMillis());
|
||||
|
||||
db.addReaction(messageId, reaction);
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, isMms, reaction, false));
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "[sendNewReaction] Could not find message! Ignoring.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendReactionRemoval(@NonNull Context context, long messageId, boolean isMms, @NonNull ReactionRecord reaction) {
|
||||
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
db.deleteReaction(messageId, reaction.getAuthor());
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, isMms, reaction, true));
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "[sendReactionRemoval] Could not find message! Ignoring.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void resendGroupMessage(Context context, MessageRecord messageRecord, RecipientId filterRecipientId) {
|
||||
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
|
||||
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientId);
|
||||
|
44
src/org/thoughtcrime/securesms/util/AvatarUtil.java
Normal file
@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public final class AvatarUtil {
|
||||
|
||||
private AvatarUtil() {
|
||||
}
|
||||
|
||||
public static void loadIconIntoImageView(@NonNull Recipient recipient, @NonNull ImageView target) {
|
||||
Context context = target.getContext();
|
||||
String name = Optional.fromNullable(recipient.getDisplayName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
|
||||
MaterialColor fallbackColor = recipient.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
|
||||
Drawable fallback = new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(context, fallbackColor.toAvatarColor(context));
|
||||
|
||||
GlideApp.with(context)
|
||||
.load(new ProfileContactPhoto(recipient.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(context))))
|
||||
.error(fallback)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(target);
|
||||
}
|
||||
}
|