feat: Add conversation pinning (#806)

* feat: Add conversation pinning

* Update pinned conversation icon

* Update pinned conversation column name
This commit is contained in:
ceokot 2021-12-10 01:18:56 +02:00 committed by GitHub
parent 546a6ec3f7
commit 15f5ac10ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 184 additions and 31 deletions

View File

@ -90,6 +90,7 @@ public class ThreadDatabase extends Database {
public static final String EXPIRES_IN = "expires_in"; public static final String EXPIRES_IN = "expires_in";
public static final String LAST_SEEN = "last_seen"; public static final String LAST_SEEN = "last_seen";
private static final String HAS_SENT = "has_sent"; private static final String HAS_SENT = "has_sent";
public static final String IS_PINNED = "is_pinned";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
@ -109,7 +110,7 @@ public class ThreadDatabase extends Database {
private static final String[] THREAD_PROJECTION = { private static final String[] THREAD_PROJECTION = {
ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE, ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE,
SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED
}; };
private static final List<String> TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION) private static final List<String> TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION)
@ -121,6 +122,11 @@ public class ThreadDatabase extends Database {
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION)) Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
.toList(); .toList();
public static String getCreatePinnedCommand() {
return "ALTER TABLE "+ TABLE_NAME + " " +
"ADD COLUMN " + IS_PINNED + " INTEGER DEFAULT 0;";
}
public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) { public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
} }
@ -577,6 +583,16 @@ public class ThreadDatabase extends Database {
} }
} }
public void setPinned(long threadId, boolean pinned) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(IS_PINNED, pinned ? 1 : 0);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE,
new String[] {String.valueOf(threadId)});
notifyConversationListeners(threadId);
}
private boolean deleteThreadOnEmpty(long threadId) { private boolean deleteThreadOnEmpty(long threadId) {
Recipient threadRecipient = getRecipientForThreadId(threadId); Recipient threadRecipient = getRecipientForThreadId(threadId);
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient(); return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
@ -622,7 +638,7 @@ public class ThreadDatabase extends Database {
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
" WHERE " + where + " WHERE " + where +
" ORDER BY " + TABLE_NAME + "." + DATE + " DESC"; " ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + DATE + " DESC";
if (limit > 0) { if (limit > 0) {
query += " LIMIT " + limit; query += " LIMIT " + limit;
@ -683,6 +699,7 @@ public class ThreadDatabase extends Database {
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN)); long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN)); long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN));
Uri snippetUri = getSnippetUri(cursor); Uri snippetUri = getSnippetUri(cursor);
boolean pinned = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.IS_PINNED)) != 0;
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0; readReceiptCount = 0;
@ -690,7 +707,7 @@ public class ThreadDatabase extends Database {
return new ThreadRecord(body, snippetUri, recipient, date, count, return new ThreadRecord(body, snippetUri, recipient, date, count,
unreadCount, threadId, deliveryReceiptCount, status, type, unreadCount, threadId, deliveryReceiptCount, status, type,
distributionType, archived, expiresIn, lastSeen, readReceiptCount); distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
} }
private @Nullable Uri getSnippetUri(Cursor cursor) { private @Nullable Uri getSnippetUri(Cursor cursor) {

View File

@ -60,9 +60,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV26 = 47; private static final int lokiV26 = 47;
private static final int lokiV27 = 48; private static final int lokiV27 = 48;
private static final int lokiV28 = 49; private static final int lokiV28 = 49;
private static final int lokiV29 = 50;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV28; private static final int DATABASE_VERSION = lokiV29;
private static final String DATABASE_NAME = "signal.db"; private static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -134,6 +135,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable()); db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand()); db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand());
db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand()); db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand());
db.execSQL(ThreadDatabase.getCreatePinnedCommand());
executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -308,6 +310,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiMessageDatabase.getCreateMessageHashTableCommand()); db.execSQL(LokiMessageDatabase.getCreateMessageHashTableCommand());
} }
if (oldVersion < lokiV29) {
db.execSQL(ThreadDatabase.getCreatePinnedCommand());
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -49,12 +49,13 @@ public class ThreadRecord extends DisplayRecord {
private final boolean archived; private final boolean archived;
private final long expiresIn; private final long expiresIn;
private final long lastSeen; private final long lastSeen;
private final boolean pinned;
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
@NonNull Recipient recipient, long date, long count, int unreadCount, @NonNull Recipient recipient, long date, long count, int unreadCount,
long threadId, int deliveryReceiptCount, int status, long snippetType, long threadId, int deliveryReceiptCount, int status, long snippetType,
int distributionType, boolean archived, long expiresIn, long lastSeen, int distributionType, boolean archived, long expiresIn, long lastSeen,
int readReceiptCount) int readReceiptCount, boolean pinned)
{ {
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
this.snippetUri = snippetUri; this.snippetUri = snippetUri;
@ -64,6 +65,7 @@ public class ThreadRecord extends DisplayRecord {
this.archived = archived; this.archived = archived;
this.expiresIn = expiresIn; this.expiresIn = expiresIn;
this.lastSeen = lastSeen; this.lastSeen = lastSeen;
this.pinned = pinned;
} }
public @Nullable Uri getSnippetUri() { public @Nullable Uri getSnippetUri() {
@ -163,4 +165,8 @@ public class ThreadRecord extends DisplayRecord {
public long getLastSeen() { public long getLastSeen() {
return lastSeen; return lastSeen;
} }
public boolean isPinned() {
return pinned;
}
} }

View File

@ -8,18 +8,20 @@ import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.* import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.UiModeUtilities
public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener { public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
//FIXME AC: Supplying a recipient directly into the field from an activity //FIXME AC: Supplying a threadRecord directly into the field from an activity
// is not the best idea. It doesn't survive configuration change. // is not the best idea. It doesn't survive configuration change.
// We should be dealing with IDs and all sorts of serializable data instead // We should be dealing with IDs and all sorts of serializable data instead
// if we want to use dialog fragments properly. // if we want to use dialog fragments properly.
lateinit var recipient: Recipient lateinit var thread: ThreadRecord
var onViewDetailsTapped: (() -> Unit?)? = null var onViewDetailsTapped: (() -> Unit?)? = null
var onPinTapped: (() -> Unit)? = null
var onUnpinTapped: (() -> Unit)? = null
var onBlockTapped: (() -> Unit)? = null var onBlockTapped: (() -> Unit)? = null
var onUnblockTapped: (() -> Unit)? = null var onUnblockTapped: (() -> Unit)? = null
var onDeleteTapped: (() -> Unit)? = null var onDeleteTapped: (() -> Unit)? = null
@ -33,6 +35,8 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
override fun onClick(v: View?) { override fun onClick(v: View?) {
when (v) { when (v) {
detailsTextView -> onViewDetailsTapped?.invoke() detailsTextView -> onViewDetailsTapped?.invoke()
pinTextView -> onPinTapped?.invoke()
unpinTextView -> onUnpinTapped?.invoke()
blockTextView -> onBlockTapped?.invoke() blockTextView -> onBlockTapped?.invoke()
unblockTextView -> onUnblockTapped?.invoke() unblockTextView -> onUnblockTapped?.invoke()
deleteTextView -> onDeleteTapped?.invoke() deleteTextView -> onDeleteTapped?.invoke()
@ -44,7 +48,8 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
if (!this::recipient.isInitialized) { return dismiss() } if (!this::thread.isInitialized) { return dismiss() }
val recipient = thread.recipient
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) { if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
detailsTextView.visibility = View.VISIBLE detailsTextView.visibility = View.VISIBLE
unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
@ -62,6 +67,10 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.
notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
notificationsTextView.setOnClickListener(this) notificationsTextView.setOnClickListener(this)
deleteTextView.setOnClickListener(this) deleteTextView.setOnClickListener(this)
pinTextView.isVisible = !thread.isPinned
unpinTextView.isVisible = thread.isPinned
pinTextView.setOnClickListener(this)
unpinTextView.setOnClickListener(this)
} }
override fun onStart() { override fun onStart() {

View File

@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import java.util.* import java.util.Locale
class ConversationView : LinearLayout { class ConversationView : LinearLayout {
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
@ -39,6 +39,13 @@ class ConversationView : LinearLayout {
// region Updating // region Updating
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) { fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
this.thread = thread this.thread = thread
if (thread.isPinned) {
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0)
background = ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background)
} else {
conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
background = ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
}
profilePictureView.glide = glide profilePictureView.glide = glide
val unreadCount = thread.unreadCount val unreadCount = thread.unreadCount
if (thread.recipient.isBlocked) { if (thread.recipient.isBlocked) {

View File

@ -59,11 +59,11 @@ import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.util.* import org.thoughtcrime.securesms.util.*
import java.io.IOException import java.io.IOException
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener,
SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks<Cursor> {
private lateinit var glide: GlideRequests private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null private var broadcastReceiver: BroadcastReceiver? = null
@ -74,6 +74,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
private val publicKey: String private val publicKey: String
get() = TextSecurePreferences.getLocalNumber(this)!! get() = TextSecurePreferences.getLocalNumber(this)!!
private val homeAdapter:HomeAdapter by lazy {
HomeAdapter(this, threadDb.conversationList)
}
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
@ -104,8 +108,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
seedReminderStub.isVisible = false seedReminderStub.isVisible = false
} }
// Set up recycler view // Set up recycler view
val cursor = threadDb.conversationList
val homeAdapter = HomeAdapter(this, cursor)
homeAdapter.setHasStableIds(true) homeAdapter.setHasStableIds(true)
homeAdapter.glide = glide homeAdapter.glide = glide
homeAdapter.conversationClickListener = this homeAdapter.conversationClickListener = this
@ -115,21 +117,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
IP2Country.configureIfNeeded(this@HomeActivity) IP2Country.configureIfNeeded(this@HomeActivity)
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will) // This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> { LoaderManager.getInstance(this).restartLoader(0, null, this)
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
return HomeLoader(this@HomeActivity)
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
homeAdapter.changeCursor(cursor)
updateEmptyState()
}
override fun onLoaderReset(cursor: Loader<Cursor>) {
homeAdapter.changeCursor(null)
}
})
// Set up new conversation button set // Set up new conversation button set
newConversationButtonSet.delegate = this newConversationButtonSet.delegate = this
// Observe blocked contacts changed events // Observe blocked contacts changed events
@ -170,6 +158,19 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
EventBus.getDefault().register(this@HomeActivity) EventBus.getDefault().register(this@HomeActivity)
} }
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
return HomeLoader(this@HomeActivity)
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
homeAdapter.changeCursor(cursor)
updateEmptyState()
}
override fun onLoaderReset(cursor: Loader<Cursor>) {
homeAdapter.changeCursor(null)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
@ -245,7 +246,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
override fun onLongConversationClick(view: ConversationView) { override fun onLongConversationClick(view: ConversationView) {
val thread = view.thread ?: return val thread = view.thread ?: return
val bottomSheet = ConversationOptionsBottomSheet() val bottomSheet = ConversationOptionsBottomSheet()
bottomSheet.recipient = thread.recipient bottomSheet.thread = thread
bottomSheet.onViewDetailsTapped = { bottomSheet.onViewDetailsTapped = {
bottomSheet.dismiss() bottomSheet.dismiss()
val userDetailsBottomSheet = UserDetailsBottomSheet() val userDetailsBottomSheet = UserDetailsBottomSheet()
@ -280,6 +281,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
setNotifyType(thread, notifyType) setNotifyType(thread, notifyType)
} }
} }
bottomSheet.onPinTapped = {
bottomSheet.dismiss()
if (!thread.isPinned) {
pinConversation(thread)
}
}
bottomSheet.onUnpinTapped = {
bottomSheet.dismiss()
if (thread.isPinned) {
unpinConversation(thread)
}
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag) bottomSheet.show(supportFragmentManager, bottomSheet.tag)
} }
@ -344,6 +357,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
} }
} }
private fun pinConversation(thread: ThreadRecord) {
ThreadUtils.queue {
threadDb.setPinned(thread.threadId, true)
Util.runOnMain {
LoaderManager.getInstance(this).restartLoader(0, null, this)
}
}
}
private fun unpinConversation(thread: ThreadRecord) {
ThreadUtils.queue {
threadDb.setPinned(thread.threadId, false)
Util.runOnMain {
LoaderManager.getInstance(this).restartLoader(0, null, this)
}
}
}
private fun deleteConversation(thread: ThreadRecord) { private fun deleteConversation(thread: ThreadRecord) {
val threadID = thread.threadId val threadID = thread.threadId
val recipient = thread.recipient val recipient = thread.recipient

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/cell_selected">
<item>
<color android:color="?attr/conversation_pinned_background_color" />
</item>
</ripple>

View File

@ -0,0 +1,11 @@
<!-- drawable/pin_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.8,14L10,12.8V4H14V12.8L15.2,14H8.8Z" />
</vector>

View File

@ -0,0 +1,11 @@
<!-- drawable/pin_off_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8,6.2V4H7V2H17V4H16V12L18,14V16H17.8L14,12.2V4H10V8.2L8,6.2M20,20.7L18.7,22L12.8,16.1V22H11.2V16H6V14L8,12V11.3L2,5.3L3.3,4L20,20.7M8.8,14H10.6L9.7,13.1L8.8,14Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="122.879"
android:viewportHeight="122.867"
android:tint="?attr/conversation_pinned_icon_color">
<path
android:fillColor="@android:color/white"
android:pathData="M83.88,0.451 L122.427,39a1.55,1.55 0,0 1,0 2.188l-13.128,13.125a1.546,1.546 0,0 1,-2.187 0l-3.732,-3.73 -17.303,17.3c3.882,14.621 0.095,30.857 -11.37,42.32 -0.266,0.268 -0.535,0.529 -0.808,0.787 -1.004,0.955 -0.843,0.949 -1.813,-0.021L47.597,86.48 0,122.867l36.399,-47.584L11.874,50.76c-0.978,-0.98 -0.896,-0.826 0.066,-1.837 0.24,-0.251 0.485,-0.503 0.734,-0.753C24.137,36.707 40.376,32.917 54.996,36.8l17.301,-17.3 -3.733,-3.732a1.553,1.553 0,0 1,0 -2.188L81.691,0.451a1.554,1.554 0,0 1,2.189 0z" />
</vector>

View File

@ -16,6 +16,22 @@
android:drawableTint="?attr/colorControlNormal" android:drawableTint="?attr/colorControlNormal"
android:text="@string/details" /> android:text="@string/details" />
<TextView
android:id="@+id/pinTextView"
style="@style/BottomSheetActionItem"
android:drawableStart="?attr/menu_pin_icon"
android:text="@string/conversation_pin"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/unpinTextView"
style="@style/BottomSheetActionItem"
android:drawableStart="?attr/menu_unpin_icon"
android:text="@string/conversation_unpin"
android:visibility="gone"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/blockTextView" android:id="@+id/blockTextView"
style="@style/BottomSheetActionItem" style="@style/BottomSheetActionItem"

View File

@ -5,7 +5,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/conversation_view_background"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
@ -52,12 +51,14 @@
android:id="@+id/conversationViewDisplayNameTextView" android:id="@+id/conversationViewDisplayNameTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textSize="@dimen/medium_font_size" android:textSize="@dimen/medium_font_size"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/text" android:textColor="@color/text"
tools:drawableRight="@drawable/ic_pin"
tools:text="I'm a very long display name. What are you going to do about it?" /> tools:text="I'm a very long display name. What are you going to do about it?" />
<RelativeLayout <RelativeLayout

View File

@ -31,6 +31,8 @@
<color name="scroll_to_bottom_button_background">#FCFCFC</color> <color name="scroll_to_bottom_button_background">#FCFCFC</color>
<color name="scroll_to_bottom_button_border">#99000000</color> <color name="scroll_to_bottom_button_border">#99000000</color>
<color name="conversation_unread_count_indicator_background">#E0E0E0</color> <color name="conversation_unread_count_indicator_background">#E0E0E0</color>
<color name="conversation_pinned_background">#F0F0F0</color>
<color name="conversation_pinned_icon">#606060</color>
<color name="default_background_start">#ffffff</color> <color name="default_background_start">#ffffff</color>
<color name="default_background_end">#fcfcfc</color> <color name="default_background_end">#fcfcfc</color>

View File

@ -87,6 +87,8 @@
<attr name="conversation_item_audio_seek_bar_color_incoming" format="reference|color" /> <attr name="conversation_item_audio_seek_bar_color_incoming" format="reference|color" />
<attr name="conversation_item_audio_seek_bar_color_outgoing" format="reference|color" /> <attr name="conversation_item_audio_seek_bar_color_outgoing" format="reference|color" />
<attr name="conversation_item_audio_seek_bar_background_color" format="reference|color" /> <attr name="conversation_item_audio_seek_bar_background_color" format="reference|color" />
<attr name="conversation_pinned_background_color" format="reference|color" />
<attr name="conversation_pinned_icon_color" format="reference|color" />
<attr name="dialog_info_icon" format="reference" /> <attr name="dialog_info_icon" format="reference" />
<attr name="dialog_alert_icon" format="reference" /> <attr name="dialog_alert_icon" format="reference" />
@ -117,6 +119,8 @@
<attr name="menu_photo_library_icon" format="reference" /> <attr name="menu_photo_library_icon" format="reference" />
<attr name="menu_delete_icon" format="reference" /> <attr name="menu_delete_icon" format="reference" />
<attr name="menu_info_icon" format="reference" /> <attr name="menu_info_icon" format="reference" />
<attr name="menu_pin_icon" format="reference" />
<attr name="menu_unpin_icon" format="reference" />
<attr name="pref_icon_tint" format="color"/> <attr name="pref_icon_tint" format="color"/>

View File

@ -37,6 +37,8 @@
<color name="scroll_to_bottom_button_background">#171717</color> <color name="scroll_to_bottom_button_background">#171717</color>
<color name="scroll_to_bottom_button_border">#99FFFFFF</color> <color name="scroll_to_bottom_button_border">#99FFFFFF</color>
<color name="conversation_unread_count_indicator_background">#303030</color> <color name="conversation_unread_count_indicator_background">#303030</color>
<color name="conversation_pinned_background">#404040</color>
<color name="conversation_pinned_icon">#B3B3B3</color>
<array name="profile_picture_placeholder_colors"> <array name="profile_picture_placeholder_colors">
<item>#5ff8b0</item> <item>#5ff8b0</item>

View File

@ -902,5 +902,7 @@
<string name="activity_settings_support">Debug Log</string> <string name="activity_settings_support">Debug Log</string>
<string name="dialog_share_logs_title">Share Logs</string> <string name="dialog_share_logs_title">Share Logs</string>
<string name="dialog_share_logs_explanation">Would you like to export your application logs to be able to share for troubleshooting?</string> <string name="dialog_share_logs_explanation">Would you like to export your application logs to be able to share for troubleshooting?</string>
<string name="conversation_pin">Pin</string>
<string name="conversation_unpin">Unpin</string>
</resources> </resources>

View File

@ -76,6 +76,8 @@
<item name="menu_split_icon">@drawable/ic_baseline_call_split_24</item> <item name="menu_split_icon">@drawable/ic_baseline_call_split_24</item>
<item name="menu_popup_expand">@drawable/ic_baseline_launch_24</item> <item name="menu_popup_expand">@drawable/ic_baseline_launch_24</item>
<item name="menu_info_icon">@drawable/ic_baseline_info_24</item> <item name="menu_info_icon">@drawable/ic_baseline_info_24</item>
<item name="menu_pin_icon">@drawable/ic_outline_pin_24</item>
<item name="menu_unpin_icon">@drawable/ic_outline_pin_off_24</item>
<item name="conversation_emoji_toggle">@drawable/ic_emoji_filled_keyboard_24</item> <item name="conversation_emoji_toggle">@drawable/ic_emoji_filled_keyboard_24</item>
<item name="conversation_sticker_toggle">@drawable/ic_sticker_filled_keyboard_24</item> <item name="conversation_sticker_toggle">@drawable/ic_sticker_filled_keyboard_24</item>
@ -88,6 +90,9 @@
<item name="conversation_item_audio_seek_bar_color_incoming">@color/accent</item> <item name="conversation_item_audio_seek_bar_color_incoming">@color/accent</item>
<item name="conversation_item_audio_seek_bar_color_outgoing">@color/accent</item> <item name="conversation_item_audio_seek_bar_color_outgoing">@color/accent</item>
<item name="conversation_item_audio_seek_bar_background_color">@color/text</item> <item name="conversation_item_audio_seek_bar_background_color">@color/text</item>
<item name="conversation_pinned_background_color">@color/conversation_pinned_background</item>
<item name="conversation_pinned_icon_color">@color/conversation_pinned_icon</item>
</style> </style>
<!-- This should be the default theme for the application. --> <!-- This should be the default theme for the application. -->

View File

@ -87,6 +87,8 @@
<attr name="conversation_item_audio_seek_bar_color_incoming" format="reference|color" /> <attr name="conversation_item_audio_seek_bar_color_incoming" format="reference|color" />
<attr name="conversation_item_audio_seek_bar_color_outgoing" format="reference|color" /> <attr name="conversation_item_audio_seek_bar_color_outgoing" format="reference|color" />
<attr name="conversation_item_audio_seek_bar_background_color" format="reference|color" /> <attr name="conversation_item_audio_seek_bar_background_color" format="reference|color" />
<attr name="conversation_pinned_background_color" format="reference|color" />
<attr name="conversation_pinned_icon_color" format="reference|color" />
<attr name="dialog_info_icon" format="reference" /> <attr name="dialog_info_icon" format="reference" />
<attr name="dialog_alert_icon" format="reference" /> <attr name="dialog_alert_icon" format="reference" />
@ -117,6 +119,8 @@
<attr name="menu_photo_library_icon" format="reference" /> <attr name="menu_photo_library_icon" format="reference" />
<attr name="menu_delete_icon" format="reference" /> <attr name="menu_delete_icon" format="reference" />
<attr name="menu_info_icon" format="reference" /> <attr name="menu_info_icon" format="reference" />
<attr name="menu_pin_icon" format="reference" />
<attr name="menu_unpin_icon" format="reference" />
<attr name="pref_icon_tint" format="color"/> <attr name="pref_icon_tint" format="color"/>