From 15f5ac10ec5abb195b19c5d245e37dd5f351b6db Mon Sep 17 00:00:00 2001 From: ceokot Date: Fri, 10 Dec 2021 01:18:56 +0200 Subject: [PATCH] feat: Add conversation pinning (#806) * feat: Add conversation pinning * Update pinned conversation icon * Update pinned conversation column name --- .../securesms/database/ThreadDatabase.java | 23 +++++- .../database/helpers/SQLCipherOpenHelper.java | 8 ++- .../database/model/ThreadRecord.java | 8 ++- .../home/ConversationOptionsBottomSheet.kt | 17 +++-- .../securesms/home/ConversationView.kt | 9 ++- .../securesms/home/HomeActivity.kt | 71 +++++++++++++------ .../conversation_pinned_background.xml | 9 +++ .../main/res/drawable/ic_outline_pin_24.xml | 11 +++ .../res/drawable/ic_outline_pin_off_24.xml | 11 +++ app/src/main/res/drawable/ic_pin.xml | 10 +++ .../fragment_conversation_bottom_sheet.xml | 16 +++++ app/src/main/res/layout/view_conversation.xml | 3 +- .../main/res/values-notnight-v21/colors.xml | 2 + app/src/main/res/values/attrs.xml | 4 ++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 5 ++ libsession/src/main/res/values/attrs.xml | 4 ++ 18 files changed, 184 insertions(+), 31 deletions(-) create mode 100644 app/src/main/res/drawable/conversation_pinned_background.xml create mode 100644 app/src/main/res/drawable/ic_outline_pin_24.xml create mode 100644 app/src/main/res/drawable/ic_outline_pin_off_24.xml create mode 100644 app/src/main/res/drawable/ic_pin.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index f2adb0554..2aa02a80d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -90,6 +90,7 @@ public class ThreadDatabase extends Database { public static final String EXPIRES_IN = "expires_in"; public static final String LAST_SEEN = "last_seen"; 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 + " (" + ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + @@ -109,7 +110,7 @@ public class ThreadDatabase extends Database { private static final String[] THREAD_PROJECTION = { 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 TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION) @@ -121,6 +122,11 @@ public class ThreadDatabase extends Database { Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION)) .toList(); + public static String getCreatePinnedCommand() { + return "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + IS_PINNED + " INTEGER DEFAULT 0;"; + } + public ThreadDatabase(Context context, SQLCipherOpenHelper 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) { Recipient threadRecipient = getRecipientForThreadId(threadId); return threadRecipient != null && !threadRecipient.isOpenGroupRecipient(); @@ -622,7 +638,7 @@ public class ThreadDatabase extends Database { " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " WHERE " + where + - " ORDER BY " + TABLE_NAME + "." + DATE + " DESC"; + " ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + DATE + " DESC"; if (limit > 0) { query += " LIMIT " + limit; @@ -683,6 +699,7 @@ public class ThreadDatabase extends Database { long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN)); long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN)); Uri snippetUri = getSnippetUri(cursor); + boolean pinned = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.IS_PINNED)) != 0; if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { readReceiptCount = 0; @@ -690,7 +707,7 @@ public class ThreadDatabase extends Database { return new ThreadRecord(body, snippetUri, recipient, date, count, unreadCount, threadId, deliveryReceiptCount, status, type, - distributionType, archived, expiresIn, lastSeen, readReceiptCount); + distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned); } private @Nullable Uri getSnippetUri(Cursor cursor) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index ccf5c9e77..e563e6e5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -60,9 +60,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV26 = 47; private static final int lokiV27 = 48; 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 - private static final int DATABASE_VERSION = lokiV28; + private static final int DATABASE_VERSION = lokiV29; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -134,6 +135,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable()); db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand()); db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand()); + db.execSQL(ThreadDatabase.getCreatePinnedCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -308,6 +310,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiMessageDatabase.getCreateMessageHashTableCommand()); } + if (oldVersion < lokiV29) { + db.execSQL(ThreadDatabase.getCreatePinnedCommand()); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 91c272957..86aaf9f12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -49,12 +49,13 @@ public class ThreadRecord extends DisplayRecord { private final boolean archived; private final long expiresIn; private final long lastSeen; + private final boolean pinned; public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, @NonNull Recipient recipient, long date, long count, int unreadCount, long threadId, int deliveryReceiptCount, int status, long snippetType, int distributionType, boolean archived, long expiresIn, long lastSeen, - int readReceiptCount) + int readReceiptCount, boolean pinned) { super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); this.snippetUri = snippetUri; @@ -64,6 +65,7 @@ public class ThreadRecord extends DisplayRecord { this.archived = archived; this.expiresIn = expiresIn; this.lastSeen = lastSeen; + this.pinned = pinned; } public @Nullable Uri getSnippetUri() { @@ -163,4 +165,8 @@ public class ThreadRecord extends DisplayRecord { public long getLastSeen() { return lastSeen; } + + public boolean isPinned() { + return pinned; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt index 9d0f32847..40f41d23d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt @@ -8,18 +8,20 @@ import androidx.core.view.isVisible import com.google.android.material.bottomsheet.BottomSheetDialogFragment import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.* 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 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. // We should be dealing with IDs and all sorts of serializable data instead // if we want to use dialog fragments properly. - lateinit var recipient: Recipient + lateinit var thread: ThreadRecord var onViewDetailsTapped: (() -> Unit?)? = null + var onPinTapped: (() -> Unit)? = null + var onUnpinTapped: (() -> Unit)? = null var onBlockTapped: (() -> Unit)? = null var onUnblockTapped: (() -> Unit)? = null var onDeleteTapped: (() -> Unit)? = null @@ -33,6 +35,8 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View. override fun onClick(v: View?) { when (v) { detailsTextView -> onViewDetailsTapped?.invoke() + pinTextView -> onPinTapped?.invoke() + unpinTextView -> onUnpinTapped?.invoke() blockTextView -> onBlockTapped?.invoke() unblockTextView -> onUnblockTapped?.invoke() deleteTextView -> onDeleteTapped?.invoke() @@ -44,7 +48,8 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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) { detailsTextView.visibility = View.VISIBLE 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.setOnClickListener(this) deleteTextView.setOnClickListener(this) + pinTextView.isVisible = !thread.isPinned + unpinTextView.isVisible = thread.isPinned + pinTextView.setOnClickListener(this) + unpinTextView.setOnClickListener(this) } override fun onStart() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index dbc5710ad..679b26d6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.DateUtils -import java.util.* +import java.util.Locale class ConversationView : LinearLayout { private val screenWidth = Resources.getSystem().displayMetrics.widthPixels @@ -39,6 +39,13 @@ class ConversationView : LinearLayout { // region Updating fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) { 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 val unreadCount = thread.unreadCount if (thread.recipient.isBlocked) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 6cf4aeafb..e0fad535e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -59,11 +59,11 @@ import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.util.* import java.io.IOException -import java.util.* import javax.inject.Inject @AndroidEntryPoint -class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { +class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, + SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks { private lateinit var glide: GlideRequests private var broadcastReceiver: BroadcastReceiver? = null @@ -74,6 +74,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis private val publicKey: String get() = TextSecurePreferences.getLocalNumber(this)!! + private val homeAdapter:HomeAdapter by lazy { + HomeAdapter(this, threadDb.conversationList) + } + // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) @@ -104,8 +108,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis seedReminderStub.isVisible = false } // Set up recycler view - val cursor = threadDb.conversationList - val homeAdapter = HomeAdapter(this, cursor) homeAdapter.setHasStableIds(true) homeAdapter.glide = glide homeAdapter.conversationClickListener = this @@ -115,21 +117,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } IP2Country.configureIfNeeded(this@HomeActivity) // 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 { - - override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { - return HomeLoader(this@HomeActivity) - } - - override fun onLoadFinished(loader: Loader, cursor: Cursor?) { - homeAdapter.changeCursor(cursor) - updateEmptyState() - } - - override fun onLoaderReset(cursor: Loader) { - homeAdapter.changeCursor(null) - } - }) + LoaderManager.getInstance(this).restartLoader(0, null, this) // Set up new conversation button set newConversationButtonSet.delegate = this // Observe blocked contacts changed events @@ -170,6 +158,19 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis EventBus.getDefault().register(this@HomeActivity) } + override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { + return HomeLoader(this@HomeActivity) + } + + override fun onLoadFinished(loader: Loader, cursor: Cursor?) { + homeAdapter.changeCursor(cursor) + updateEmptyState() + } + + override fun onLoaderReset(cursor: Loader) { + homeAdapter.changeCursor(null) + } + override fun onResume() { super.onResume() ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) @@ -245,7 +246,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis override fun onLongConversationClick(view: ConversationView) { val thread = view.thread ?: return val bottomSheet = ConversationOptionsBottomSheet() - bottomSheet.recipient = thread.recipient + bottomSheet.thread = thread bottomSheet.onViewDetailsTapped = { bottomSheet.dismiss() val userDetailsBottomSheet = UserDetailsBottomSheet() @@ -280,6 +281,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis 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) } @@ -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) { val threadID = thread.threadId val recipient = thread.recipient diff --git a/app/src/main/res/drawable/conversation_pinned_background.xml b/app/src/main/res/drawable/conversation_pinned_background.xml new file mode 100644 index 000000000..9a435d05f --- /dev/null +++ b/app/src/main/res/drawable/conversation_pinned_background.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_outline_pin_24.xml b/app/src/main/res/drawable/ic_outline_pin_24.xml new file mode 100644 index 000000000..d96e4236f --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_pin_24.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_outline_pin_off_24.xml b/app/src/main/res/drawable/ic_outline_pin_off_24.xml new file mode 100644 index 000000000..056009e49 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_pin_off_24.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml new file mode 100644 index 000000000..390fe36f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_pin.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml b/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml index ad1044a53..a9ea22680 100644 --- a/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml +++ b/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml @@ -16,6 +16,22 @@ android:drawableTint="?attr/colorControlNormal" android:text="@string/details" /> + + + + @@ -52,12 +51,14 @@ android:id="@+id/conversationViewDisplayNameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:drawablePadding="4dp" android:maxLines="1" android:ellipsize="end" android:textAlignment="viewStart" android:textSize="@dimen/medium_font_size" android:textStyle="bold" 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?" /> #FCFCFC #99000000 #E0E0E0 + #F0F0F0 + #606060 #ffffff #fcfcfc diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index f21ca1c70..9ece70ec1 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -87,6 +87,8 @@ + + @@ -117,6 +119,8 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index feb143227..b16b9029f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -37,6 +37,8 @@ #171717 #99FFFFFF #303030 + #404040 + #B3B3B3 #5ff8b0 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1319eeb37..8c4994d5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -902,5 +902,7 @@ Debug Log Share Logs Would you like to export your application logs to be able to share for troubleshooting? + Pin + Unpin diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 3c81efc7e..0b7ac4c14 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -76,6 +76,8 @@ @drawable/ic_baseline_call_split_24 @drawable/ic_baseline_launch_24 @drawable/ic_baseline_info_24 + @drawable/ic_outline_pin_24 + @drawable/ic_outline_pin_off_24 @drawable/ic_emoji_filled_keyboard_24 @drawable/ic_sticker_filled_keyboard_24 @@ -88,6 +90,9 @@ @color/accent @color/accent @color/text + + @color/conversation_pinned_background + @color/conversation_pinned_icon diff --git a/libsession/src/main/res/values/attrs.xml b/libsession/src/main/res/values/attrs.xml index f21ca1c70..9ece70ec1 100644 --- a/libsession/src/main/res/values/attrs.xml +++ b/libsession/src/main/res/values/attrs.xml @@ -87,6 +87,8 @@ + + @@ -117,6 +119,8 @@ + +