mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 20:15:21 +00:00
Support for a "new messages" divider in conversations
// FREEBIE
This commit is contained in:
parent
0075940050
commit
d9b42c4369
BIN
res/drawable-xxhdpi/last_seen_background.9.png
Normal file
BIN
res/drawable-xxhdpi/last_seen_background.9.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 187 B |
7
res/drawable/last_seen_divider_text_background.xml
Normal file
7
res/drawable/last_seen_divider_text_background.xml
Normal 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>
|
23
res/layout/conversation_item_last_seen.xml
Normal file
23
res/layout/conversation_item_last_seen.xml
Normal 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>
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user