Support for a "new messages" divider in conversations

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2017-02-13 22:35:47 -08:00
parent 0075940050
commit d9b42c4369
20 changed files with 278 additions and 33 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/white"/>
<corners android:radius="65dp"/>
<padding android:bottom="15dp" android:left="15dp" android:right="15dp" android:top="15dp"/>
</shape>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_marginBottom="5dp"
android:background="@drawable/last_seen_background">
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:textSize="12sp"
android:padding="12dp"
android:background="@drawable/last_seen_divider_text_background"
tools:text="3 unread messages" />
</FrameLayout>

View File

@ -146,6 +146,12 @@
<string name="ConversationActivity_error_sending_voice_message">Error sending voice message</string> <string name="ConversationActivity_error_sending_voice_message">Error sending voice message</string>
<string name="ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device">There is no app available to handle this link on your device.</string> <string name="ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device">There is no app available to handle this link on your device.</string>
<!-- ConversationAdapter -->
<plurals name="ConversationAdapter_n_unread_messages">
<item quantity="one">%d unread message</item>
<item quantity="other">%d unread messages</item>
</plurals>
<!-- ConversationFragment --> <!-- ConversationFragment -->
<string name="ConversationFragment_message_details">Message details</string> <string name="ConversationFragment_message_details">Message details</string>
<string name="ConversationFragment_transport_s_sent_received_s">Transport: %1$s\nSent/Received: %2$s</string> <string name="ConversationFragment_transport_s_sent_received_s">Transport: %1$s\nSent/Received: %2$s</string>
@ -1310,7 +1316,6 @@
<!-- transport_selection_list_item --> <!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string> <string name="transport_selection_list_item__transport_icon">Transport icon</string>
<!-- EOF --> <!-- EOF -->
</resources> </resources>

View File

@ -184,6 +184,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public static final String TEXT_EXTRA = "draft_text"; public static final String TEXT_EXTRA = "draft_text";
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type"; public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
public static final String TIMING_EXTRA = "timing"; public static final String TIMING_EXTRA = "timing";
public static final String LAST_SEEN_EXTRA = "last_seen";
private static final int PICK_IMAGE = 1; private static final int PICK_IMAGE = 1;
private static final int PICK_VIDEO = 2; private static final int PICK_VIDEO = 2;
@ -321,6 +322,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
quickAttachmentDrawer.onPause(); quickAttachmentDrawer.onPause();
inputPanel.onPause(); inputPanel.onPause();
fragment.setLastSeen(System.currentTimeMillis());
markLastSeen();
AudioSlidePlayer.stopAll(); AudioSlidePlayer.stopAll();
} }
@ -541,9 +544,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipients(), System.currentTimeMillis(), expirationTime * 1000); OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipients(), System.currentTimeMillis(), expirationTime * 1000);
MessageSender.send(ConversationActivity.this, masterSecret, outgoingMessage, threadId, false); MessageSender.send(ConversationActivity.this, masterSecret, outgoingMessage, threadId, false);
invalidateOptionsMenu();
return null; return null;
} }
@Override
protected void onPostExecute(Void result) {
invalidateOptionsMenu();
if (fragment != null) fragment.setLastSeen(0);
}
}.execute(); }.execute();
} }
}); });
@ -1421,6 +1429,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(threadId); }.execute(threadId);
} }
private void markLastSeen() {
new AsyncTask<Long, Void, Void>() {
@Override
protected Void doInBackground(Long... params) {
DatabaseFactory.getThreadDatabase(ConversationActivity.this).setLastSeen(params[0]);
return null;
}
}.execute(threadId);
}
protected void sendComplete(long threadId) { protected void sendComplete(long threadId) {
boolean refreshFragment = (threadId != this.threadId); boolean refreshFragment = (threadId != this.threadId);
this.threadId = threadId; this.threadId = threadId;
@ -1429,6 +1447,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return; return;
} }
fragment.setLastSeen(0);
if (refreshFragment) { if (refreshFragment) {
fragment.reload(recipients, threadId); fragment.reload(recipients, threadId);
MessageNotifier.setVisibleThread(threadId); MessageNotifier.setVisibleThread(threadId);

View File

@ -18,6 +18,8 @@ package org.thoughtcrime.securesms;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.LayoutRes; import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -104,15 +106,15 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
} }
protected static class HeaderViewHolder extends RecyclerView.ViewHolder { static class HeaderViewHolder extends RecyclerView.ViewHolder {
protected TextView textView; TextView textView;
public HeaderViewHolder(View itemView) { HeaderViewHolder(View itemView) {
super(itemView); super(itemView);
textView = ViewUtil.findById(itemView, R.id.text); textView = ViewUtil.findById(itemView, R.id.text);
} }
public HeaderViewHolder(TextView textView) { HeaderViewHolder(TextView textView) {
super(textView); super(textView);
this.textView = textView; this.textView = textView;
} }
@ -263,6 +265,24 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
getCursor().close(); getCursor().close();
} }
public int findLastSeenPosition(long lastSeen) {
if (lastSeen <= 0) return -1;
if (!isActiveCursor()) return -1;
int count = getItemCount();
for (int i=0;i<count;i++) {
Cursor cursor = getCursorAtPositionOrThrow(i);
MessageRecord messageRecord = getMessageRecord(cursor);
if (messageRecord.getTimestamp() < lastSeen) {
return i;
}
}
return -1;
}
public void toggleSelection(MessageRecord messageRecord) { public void toggleSelection(MessageRecord messageRecord) {
if (!batchSelected.remove(messageRecord)) { if (!batchSelected.remove(messageRecord)) {
batchSelected.add(messageRecord); batchSelected.add(messageRecord);
@ -301,15 +321,82 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR)); return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
} }
public long getTimestamp(int position) {
if (!isActiveCursor()) return 0;
if (isHeaderPosition(position)) return 0;
if (isFooterPosition(position)) return 0;
if (position >= getItemCount()) return 0;
if (position < 0) return 0;
Cursor cursor = getCursorAtPositionOrThrow(position);
MessageRecord messageRecord = getMessageRecord(cursor);
return messageRecord.getTimestamp();
}
@Override @Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false)); return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
} }
public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
}
@Override @Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
Cursor cursor = getCursorAtPositionOrThrow(position); Cursor cursor = getCursorAtPositionOrThrow(position);
viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, getMessageRecord(cursor).getDateReceived())); viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, getMessageRecord(cursor).getDateReceived()));
} }
public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) {
viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
}
public static class LastSeenHeader extends StickyHeaderDecoration {
private final ConversationAdapter adapter;
private final long lastSeenTimestamp;
public LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
super(adapter, false, false);
this.adapter = adapter;
this.lastSeenTimestamp = lastSeenTimestamp;
}
@Override
protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
if (!adapter.isActiveCursor()) {
return false;
}
if (lastSeenTimestamp <= 0) {
return false;
}
long currentRecordTimestamp = adapter.getTimestamp(position);
long previousRecordTimestamp = adapter.getTimestamp(position + 1);
return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp;
}
@Override
protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent);
adapter.onBindLastSeenViewHolder(viewHolder, position);
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
viewHolder.itemView.measure(childWidth, childHeight);
viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
return viewHolder;
}
}
} }

View File

@ -90,9 +90,12 @@ public class ConversationFragment extends Fragment
private MasterSecret masterSecret; private MasterSecret masterSecret;
private Recipients recipients; private Recipients recipients;
private long threadId; private long threadId;
private long lastSeen;
private boolean firstLoad;
private ActionMode actionMode; private ActionMode actionMode;
private Locale locale; private Locale locale;
private RecyclerView list; private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private View loadMoreView; private View loadMoreView;
private View composeDivider; private View composeDivider;
private View scrollToBottomButton; private View scrollToBottomButton;
@ -180,6 +183,8 @@ public class ConversationFragment extends Fragment
private void initializeResources() { private void initializeResources() {
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(), getActivity().getIntent().getLongArrayExtra("recipients"), true); this.recipients = RecipientFactory.getRecipientsForIds(getActivity(), getActivity().getIntent().getLongArrayExtra("recipients"), true);
this.threadId = this.getActivity().getIntent().getLongExtra("thread_id", -1); this.threadId = this.getActivity().getIntent().getLongExtra("thread_id", -1);
this.lastSeen = this.getActivity().getIntent().getLongExtra(ConversationActivity.LAST_SEEN_EXTRA, -1);
this.firstLoad = true;
OnScrollListener scrollListener = new ConversationScrollListener(getActivity()); OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
list.addOnScrollListener(scrollListener); list.addOnScrollListener(scrollListener);
@ -191,6 +196,7 @@ public class ConversationFragment extends Fragment
list.setAdapter(adapter); list.setAdapter(adapter);
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false)); list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
setLastSeen(lastSeen);
getLoaderManager().restartLoader(0, Bundle.EMPTY, this); getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
} }
} }
@ -259,6 +265,16 @@ public class ConversationFragment extends Fragment
list.smoothScrollToPosition(0); list.smoothScrollToPosition(0);
} }
public void setLastSeen(long lastSeen) {
this.lastSeen = lastSeen;
if (lastSeenDecoration != null) {
list.removeItemDecoration(lastSeenDecoration);
}
lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter(), lastSeen);
list.addItemDecoration(lastSeenDecoration);
}
private void handleCopyMessage(final Set<MessageRecord> messageRecords) { private void handleCopyMessage(final Set<MessageRecord> messageRecords) {
List<MessageRecord> messageList = new LinkedList<>(messageRecords); List<MessageRecord> messageList = new LinkedList<>(messageRecords);
Collections.sort(messageList, new Comparator<MessageRecord>() { Collections.sort(messageList, new Comparator<MessageRecord>() {
@ -389,18 +405,31 @@ public class ConversationFragment extends Fragment
@Override @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT)); return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT), lastSeen);
} }
@Override @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
ConversationLoader loader = (ConversationLoader)cursorLoader;
if (list.getAdapter() != null) { if (list.getAdapter() != null) {
if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && ((ConversationLoader)loader).hasLimit()) { if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && loader.hasLimit()) {
getListAdapter().setFooterView(loadMoreView); getListAdapter().setFooterView(loadMoreView);
} else { } else {
getListAdapter().setFooterView(null); getListAdapter().setFooterView(null);
} }
if (lastSeen == -1) {
setLastSeen(loader.getLastSeen());
}
getListAdapter().changeCursor(cursor); getListAdapter().changeCursor(cursor);
if (firstLoad) {
scrollToLastSeenPosition(lastSeen);
firstLoad = false;
}
} }
} }
@ -411,6 +440,16 @@ public class ConversationFragment extends Fragment
} }
} }
private void scrollToLastSeenPosition(long lastSeen) {
int lastSeenPosition = getListAdapter().findLastSeenPosition(lastSeen);
if (lastSeenPosition > 0) {
((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(lastSeenPosition, list.getHeight());
} else {
setLastSeen(0);
}
}
public interface ConversationFragmentListener { public interface ConversationFragmentListener {
void setThreadId(long threadId); void setThreadId(long threadId);
} }

View File

@ -161,12 +161,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
} }
@Override @Override
public void onCreateConversation(long threadId, Recipients recipients, int distributionType) { public void onCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeen) {
Intent intent = new Intent(this, ConversationActivity.class); Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds()); intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis()); intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeen);
startActivity(intent); startActivity(intent);
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);

View File

@ -54,12 +54,13 @@ public class ConversationListArchiveActivity extends PassphraseRequiredActionBar
} }
@Override @Override
public void onCreateConversation(long threadId, Recipients recipients, int distributionType) { public void onCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeenTime) {
Intent intent = new Intent(this, ConversationActivity.class); Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds()); intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.IS_ARCHIVED_EXTRA, true); intent.putExtra(ConversationActivity.IS_ARCHIVED_EXTRA, true);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeenTime);
startActivity(intent); startActivity(intent);
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);

View File

@ -307,8 +307,8 @@ public class ConversationListFragment extends Fragment
getListAdapter().getBatchSelections().size())); getListAdapter().getBatchSelections().size()));
} }
private void handleCreateConversation(long threadId, Recipients recipients, int distributionType) { private void handleCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeen) {
((ConversationSelectedListener)getActivity()).onCreateConversation(threadId, recipients, distributionType); ((ConversationSelectedListener)getActivity()).onCreateConversation(threadId, recipients, distributionType, lastSeen);
} }
@Override @Override
@ -330,7 +330,7 @@ public class ConversationListFragment extends Fragment
public void onItemClick(ConversationListItem item) { public void onItemClick(ConversationListItem item) {
if (actionMode == null) { if (actionMode == null) {
handleCreateConversation(item.getThreadId(), item.getRecipients(), handleCreateConversation(item.getThreadId(), item.getRecipients(),
item.getDistributionType()); item.getDistributionType(), item.getLastSeen());
} else { } else {
ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter(); ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
adapter.toggleThreadInBatchSet(item.getThreadId()); adapter.toggleThreadInBatchSet(item.getThreadId());
@ -361,7 +361,7 @@ public class ConversationListFragment extends Fragment
} }
public interface ConversationSelectedListener { public interface ConversationSelectedListener {
void onCreateConversation(long threadId, Recipients recipients, int distributionType); void onCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeen);
void onSwitchToArchive(); void onSwitchToArchive();
} }

View File

@ -73,6 +73,7 @@ public class ConversationListItem extends RelativeLayout
private TextView archivedView; private TextView archivedView;
private DeliveryStatusView deliveryStatusIndicator; private DeliveryStatusView deliveryStatusIndicator;
private AlertView alertView; private AlertView alertView;
private long lastSeen;
private boolean read; private boolean read;
private AvatarImageView contactPhotoImage; private AvatarImageView contactPhotoImage;
@ -119,6 +120,7 @@ public class ConversationListItem extends RelativeLayout
this.threadId = thread.getThreadId(); this.threadId = thread.getThreadId();
this.read = thread.isRead(); this.read = thread.isRead();
this.distributionType = thread.getDistributionType(); this.distributionType = thread.getDistributionType();
this.lastSeen = thread.getLastSeen();
this.recipients.addListener(this); this.recipients.addListener(this);
this.fromView.setText(recipients, read); this.fromView.setText(recipients, read);
@ -171,6 +173,10 @@ public class ConversationListItem extends RelativeLayout
return distributionType; return distributionType;
} }
public long getLastSeen() {
return lastSeen;
}
private void setThumbnailSnippet(MasterSecret masterSecret, ThreadRecord thread) { private void setThumbnailSnippet(MasterSecret masterSecret, ThreadRecord thread) {
if (thread.getSnippetUri() != null) { if (thread.getSnippetUri() != null) {
this.thumbnailView.setVisibility(View.VISIBLE); this.thumbnailView.setVisibility(View.VISIBLE);

View File

@ -73,7 +73,8 @@ public class DatabaseFactory {
private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26; private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26;
private static final int INTRODUCED_SUBSCRIPTION_ID_VERSION = 27; private static final int INTRODUCED_SUBSCRIPTION_ID_VERSION = 27;
private static final int INTRODUCED_EXPIRE_MESSAGES_VERSION = 28; private static final int INTRODUCED_EXPIRE_MESSAGES_VERSION = 28;
private static final int DATABASE_VERSION = 28; private static final int INTRODUCED_LAST_SEEN = 29;
private static final int DATABASE_VERSION = 29;
private static final String DATABASE_NAME = "messages.db"; private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); private static final Object lock = new Object();
@ -830,6 +831,10 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE thread ADD COLUMN expires_in INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE thread ADD COLUMN expires_in INTEGER DEFAULT 0");
} }
if (oldVersion < INTRODUCED_LAST_SEEN) {
db.execSQL("ALTER TABLE thread ADD COLUMN last_seen INTEGER DEFAULT 0");
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }

View File

@ -69,6 +69,7 @@ public class ThreadDatabase extends Database {
public static final String STATUS = "status"; public static final String STATUS = "status";
public static final String RECEIPT_COUNT = "delivery_receipt_count"; public static final String RECEIPT_COUNT = "delivery_receipt_count";
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 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, " +
@ -77,7 +78,8 @@ public class ThreadDatabase extends Database {
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " + TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " + SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " + ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0);"; RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
LAST_SEEN + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");", "CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");",
@ -391,6 +393,31 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners(); notifyConversationListListeners();
} }
public void setLastSeen(long threadId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
contentValues.put(LAST_SEEN, System.currentTimeMillis());
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
notifyConversationListListeners();
}
public long getLastSeen(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(0);
}
return -1;
} finally {
if (cursor != null) cursor.close();
}
}
public void deleteConversation(long threadId) { public void deleteConversation(long threadId) {
DatabaseFactory.getSmsDatabase(context).deleteThread(threadId); DatabaseFactory.getSmsDatabase(context).deleteThread(threadId);
DatabaseFactory.getMmsDatabase(context).deleteThread(threadId); DatabaseFactory.getMmsDatabase(context).deleteThread(threadId);
@ -582,11 +609,12 @@ public class ThreadDatabase extends Database {
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT)); int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT));
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));
Uri snippetUri = getSnippetUri(cursor); Uri snippetUri = getSnippetUri(cursor);
return new ThreadRecord(context, body, snippetUri, recipients, date, count, read == 1, return new ThreadRecord(context, body, snippetUri, recipients, date, count, read == 1,
threadId, receiptCount, status, type, distributionType, archived, threadId, receiptCount, status, type, distributionType, archived,
expiresIn); expiresIn, lastSeen);
} }
private DisplayRecord.Body getPlaintextBody(Cursor cursor) { private DisplayRecord.Body getPlaintextBody(Cursor cursor) {

View File

@ -9,19 +9,29 @@ import org.thoughtcrime.securesms.util.AbstractCursorLoader;
public class ConversationLoader extends AbstractCursorLoader { public class ConversationLoader extends AbstractCursorLoader {
private final long threadId; private final long threadId;
private long limit; private long limit;
private long lastSeen;
public ConversationLoader(Context context, long threadId, long limit) { public ConversationLoader(Context context, long threadId, long limit, long lastSeen) {
super(context); super(context);
this.threadId = threadId; this.threadId = threadId;
this.limit = limit; this.limit = limit;
this.lastSeen = lastSeen;
} }
public boolean hasLimit() { public boolean hasLimit() {
return limit > 0; return limit > 0;
} }
public long getLastSeen() {
return lastSeen;
}
@Override @Override
public Cursor getCursor() { public Cursor getCursor() {
if (lastSeen == -1) {
this.lastSeen = DatabaseFactory.getThreadDatabase(context).getLastSeen(threadId);
}
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, limit); return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, limit);
} }
} }

View File

@ -48,11 +48,12 @@ public class ThreadRecord extends DisplayRecord {
private final int distributionType; private final int distributionType;
private final boolean archived; private final boolean archived;
private final long expiresIn; private final long expiresIn;
private final long lastSeen;
public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri, public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri,
@NonNull Recipients recipients, long date, long count, boolean read, @NonNull Recipients recipients, long date, long count, boolean read,
long threadId, int receiptCount, int status, long snippetType, long threadId, int receiptCount, int status, long snippetType,
int distributionType, boolean archived, long expiresIn) int distributionType, boolean archived, long expiresIn, long lastSeen)
{ {
super(context, body, recipients, date, date, threadId, status, receiptCount, snippetType); super(context, body, recipients, date, date, threadId, status, receiptCount, snippetType);
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
@ -62,6 +63,7 @@ public class ThreadRecord extends DisplayRecord {
this.distributionType = distributionType; this.distributionType = distributionType;
this.archived = archived; this.archived = archived;
this.expiresIn = expiresIn; this.expiresIn = expiresIn;
this.lastSeen = lastSeen;
} }
public @Nullable Uri getSnippetUri() { public @Nullable Uri getSnippetUri() {
@ -147,4 +149,8 @@ public class ThreadRecord extends DisplayRecord {
public long getExpiresIn() { public long getExpiresIn() {
return expiresIn; return expiresIn;
} }
public long getLastSeen() {
return lastSeen;
}
} }

View File

@ -62,6 +62,7 @@ public class AndroidAutoHeardReceiver extends MasterSecretBroadcastReceiver {
for (long threadId : threadIds) { for (long threadId : threadIds) {
Log.i(TAG, "Marking meassage as read: " + threadId); Log.i(TAG, "Marking meassage as read: " + threadId);
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId); List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId);
DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
messageIdsCollection.addAll(messageIds); messageIdsCollection.addAll(messageIds);
} }

View File

@ -91,6 +91,8 @@ public class AndroidAutoReplyReceiver extends MasterSecretBroadcastReceiver {
} }
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId); List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId);
DatabaseFactory.getThreadDatabase(context).setLastSeen(replyThreadId);
MessageNotifier.updateNotification(context, masterSecret); MessageNotifier.updateNotification(context, masterSecret);
MarkReadReceiver.process(context, messageIds); MarkReadReceiver.process(context, messageIds);

View File

@ -47,6 +47,8 @@ public class MarkReadReceiver extends MasterSecretBroadcastReceiver {
Log.w(TAG, "Marking as read: " + threadId); Log.w(TAG, "Marking as read: " + threadId);
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId); List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId);
messageIdsCollection.addAll(messageIds); messageIdsCollection.addAll(messageIds);
DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
} }
process(context, messageIdsCollection); process(context, messageIdsCollection);

View File

@ -81,6 +81,8 @@ public class RemoteReplyReceiver extends MasterSecretBroadcastReceiver {
} }
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId); List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId);
DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
MessageNotifier.updateNotification(context, masterSecret); MessageNotifier.updateNotification(context, masterSecret);
MarkReadReceiver.process(context, messageIds); MarkReadReceiver.process(context, messageIds);

View File

@ -57,7 +57,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
outRect.set(0, headerHeight, 0, 0); outRect.set(0, headerHeight, 0, 0);
} }
private boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) { protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) {
boolean isReverse = isReverseLayout(parent); boolean isReverse = isReverseLayout(parent);
int itemCount = ((RecyclerView.Adapter)adapter).getItemCount(); int itemCount = ((RecyclerView.Adapter)adapter).getItemCount();
@ -74,7 +74,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
return headerId != NO_HEADER_ID && previousHeaderId != NO_HEADER_ID && headerId != previousHeaderId; return headerId != NO_HEADER_ID && previousHeaderId != NO_HEADER_ID && headerId != previousHeaderId;
} }
private ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) { protected ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) {
final long key = adapter.getHeaderId(position); final long key = adapter.getHeaderId(position);
if (headerCache.containsKey(key)) { if (headerCache.containsKey(key)) {