mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-09 08:21:48 +00:00
feat: Add conversation pinning (#806)
* feat: Add conversation pinning * Update pinned conversation icon * Update pinned conversation column name
This commit is contained in:
@@ -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<String> 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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Cursor> {
|
||||
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<Cursor> {
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
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<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() {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user