Merge branch '1628-multi-select' into 'master'

Multi-select conversion messages (to delete)

Closes #1628

See merge request briar/briar!1179
This commit is contained in:
akwizgran
2019-11-08 13:05:09 +00:00
25 changed files with 406 additions and 100 deletions

View File

@@ -97,6 +97,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
implementation 'ch.acra:acra:4.11'
implementation 'info.guardianproject.panic:panic:1.0'

View File

@@ -8,6 +8,7 @@ import android.os.Parcelable;
import android.transition.Slide;
import android.transition.Transition;
import android.util.SparseArray;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -98,7 +99,13 @@ import androidx.core.content.ContextCompat;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.selection.Selection;
import androidx.recyclerview.selection.SelectionPredicates;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.selection.SelectionTracker.SelectionObserver;
import androidx.recyclerview.selection.StorageStrategy;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import de.hdodenhof.circleimageview.CircleImageView;
import im.delight.android.identicons.IdenticonDrawable;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
@@ -120,6 +127,7 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
@@ -137,7 +145,7 @@ import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVAT
@ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, TextCache,
AttachmentCache, AttachmentListener {
AttachmentCache, AttachmentListener, ActionMode.Callback {
public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -194,8 +202,11 @@ public class ConversationActivity extends BriarActivity
private LinearLayoutManager layoutManager;
private TextInputView textInputView;
private TextSendController sendController;
private SelectionTracker<String> tracker;
@Nullable
private Parcelable layoutManagerState;
@Nullable
private ActionMode actionMode;
private volatile ContactId contactId;
@@ -257,6 +268,9 @@ public class ConversationActivity extends BriarActivity
ConversationScrollListener scrollListener =
new ConversationScrollListener(adapter, viewModel);
list.getRecyclerView().addOnScrollListener(scrollListener);
if (featureFlags.shouldEnablePrivateMessageDeletion()) {
addSelectionTracker();
}
textInputView = findViewById(R.id.text_input_container);
if (featureFlags.shouldEnableImageAttachments()) {
@@ -346,12 +360,14 @@ public class ConversationActivity extends BriarActivity
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
if (tracker != null) tracker.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
layoutManagerState = savedInstanceState.getParcelable("layoutManager");
if (tracker != null) tracker.onRestoreInstanceState(savedInstanceState);
}
@Override
@@ -409,6 +425,83 @@ public class ConversationActivity extends BriarActivity
}
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.conversation_message_actions, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // no update needed
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.action_delete) {
deleteSelectedMessages();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
tracker.clearSelection();
actionMode = null;
}
private void addSelectionTracker() {
RecyclerView recyclerView = list.getRecyclerView();
if (recyclerView.getAdapter() != adapter)
throw new IllegalStateException();
tracker = new SelectionTracker.Builder<>(
"conversationSelection",
recyclerView,
new ConversationItemKeyProvider(adapter),
new ConversationItemDetailsLookup(recyclerView),
StorageStrategy.createStringStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build();
SelectionObserver<String> observer = new SelectionObserver<String>() {
@Override
public void onItemStateChanged(String key, boolean selected) {
if (selected && actionMode == null) {
actionMode = startActionMode(ConversationActivity.this);
updateActionModeTitle();
} else if (actionMode != null) {
if (selected || tracker.hasSelection()) {
updateActionModeTitle();
} else {
actionMode.finish();
}
}
}
};
tracker.addObserver(observer);
adapter.setSelectionTracker(tracker);
}
private void updateActionModeTitle() {
if (actionMode == null) throw new IllegalStateException();
String title = String.valueOf(tracker.getSelection().size());
actionMode.setTitle(title);
}
private Collection<MessageId> getSelection() {
Selection<String> selection = tracker.getSelection();
List<MessageId> messages = new ArrayList<>(selection.size());
for (String str : selection) {
MessageId id = new MessageId(fromHexString(str));
messages.add(id);
}
return messages;
}
@UiThread
private void displayContactOnlineStatus() {
if (connectionRegistry.isConnected(contactId)) {
@@ -766,7 +859,7 @@ public class ConversationActivity extends BriarActivity
try {
boolean allDeleted =
conversationManager.deleteAllMessages(contactId);
reloadConversationAfterDeletingAllMessages(allDeleted);
reloadConversationAfterDeletingMessages(allDeleted);
} catch (DbException e) {
logException(LOG, WARNING, e);
runOnUiThreadUnlessDestroyed(() -> list.showData());
@@ -774,10 +867,28 @@ public class ConversationActivity extends BriarActivity
});
}
private void reloadConversationAfterDeletingAllMessages(
private void deleteSelectedMessages() {
list.showProgressBar();
Collection<MessageId> selected = getSelection();
// close action mode only after getting the selection
if (actionMode != null) actionMode.finish();
runOnDbThread(() -> {
try {
boolean allDeleted =
conversationManager.deleteMessages(contactId, selected);
reloadConversationAfterDeletingMessages(allDeleted);
} catch (DbException e) {
logException(LOG, WARNING, e);
runOnUiThreadUnlessDestroyed(() -> list.showData());
}
});
}
private void reloadConversationAfterDeletingMessages(
boolean allDeleted) {
runOnUiThreadUnlessDestroyed(() -> {
adapter.clear();
list.showProgressBar(); // otherwise clearing shows empty state
loadMessages();
if (!allDeleted) showNotAllDeletedDialog();
});

View File

@@ -13,12 +13,14 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter;
import org.briarproject.briar.android.util.ItemReturningAdapter;
import javax.annotation.Nullable;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@NotNullByDefault
class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
@@ -27,6 +29,8 @@ class ConversationAdapter
private ConversationListener listener;
private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration;
@Nullable
private SelectionTracker<String> tracker = null;
ConversationAdapter(Context ctx,
ConversationListener conversationListener) {
@@ -45,6 +49,17 @@ class ConversationAdapter
return item.getLayout();
}
String getItemKey(int position) {
return items.get(position).getKey();
}
int getPositionOfKey(String key) {
for (int i = 0; i < items.size(); i++) {
if (key.equals(items.get(i).getKey())) return i;
}
return NO_POSITION;
}
@Override
public ConversationItemViewHolder onCreateViewHolder(ViewGroup viewGroup,
@LayoutRes int type) {
@@ -71,7 +86,8 @@ class ConversationAdapter
@Override
public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
ConversationItem item = items.get(position);
ui.bind(item);
boolean selected = tracker != null && tracker.isSelected(item.getKey());
ui.bind(item, selected);
}
@Override
@@ -91,6 +107,10 @@ class ConversationAdapter
return c1.equals(c2);
}
void setSelectionTracker(SelectionTracker<String> tracker) {
this.tracker = tracker;
}
@Nullable
ConversationItem getLastItem() {
if (items.size() > 0) {

View File

@@ -10,6 +10,8 @@ import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@NotThreadSafe
@NotNullByDefault
abstract class ConversationItem {
@@ -45,6 +47,10 @@ abstract class ConversationItem {
return id;
}
String getKey() {
return toHexString(id.getBytes());
}
GroupId getGroupId() {
return groupId;
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.briar.android.conversation;
import android.view.MotionEvent;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.ItemDetailsLookup;
import androidx.recyclerview.widget.RecyclerView;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@NotNullByDefault
class ConversationItemDetailsLookup extends ItemDetailsLookup<String > {
private final RecyclerView recyclerView;
ConversationItemDetailsLookup(RecyclerView recyclerView) {
this.recyclerView = recyclerView;
}
@Nullable
@Override
public ItemDetails<String> getItemDetails(MotionEvent e) {
// find view that corresponds to MotionEvent
View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (view == null) return null;
// get position
int pos = recyclerView.getChildAdapterPosition(view);
if (pos == NO_POSITION) return null;
// get key ID
ConversationItemViewHolder holder =
(ConversationItemViewHolder) recyclerView.getChildViewHolder(view);
String id = holder.getItemKey();
if (id == null) return null;
return new ItemDetails<String>() {
@Override
public int getPosition() {
return pos;
}
@Override
public String getSelectionKey() {
return id;
}
};
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.briar.android.conversation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.ItemKeyProvider;
@NotNullByDefault
class ConversationItemKeyProvider extends ItemKeyProvider<String> {
private final ConversationAdapter adapter;
protected ConversationItemKeyProvider(ConversationAdapter adapter) {
super(SCOPE_MAPPED);
this.adapter = adapter;
}
@Nullable
@Override
public String getKey(int position) {
return adapter.getItemKey(position);
}
@Override
public int getPosition(String key) {
return adapter.getPositionOfKey(key);
}
}

View File

@@ -20,24 +20,31 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate;
abstract class ConversationItemViewHolder extends ViewHolder {
protected final ConversationListener listener;
private final View root;
protected final ConstraintLayout layout;
@Nullable
private final OutItemViewHolder outViewHolder;
private final TextView text;
protected final TextView time;
@Nullable
private String itemKey = null;
ConversationItemViewHolder(View v, ConversationListener listener,
boolean isIncoming) {
super(v);
this.listener = listener;
this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
root = v;
layout = v.findViewById(R.id.layout);
text = v.findViewById(R.id.text);
time = v.findViewById(R.id.time);
}
@CallSuper
void bind(ConversationItem item) {
void bind(ConversationItem item, boolean selected) {
itemKey = item.getKey();
root.setActivated(selected);
if (item.getText() != null) {
text.setText(trim(item.getText()));
}
@@ -52,4 +59,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
return outViewHolder == null;
}
@Nullable
String getItemKey() {
return itemKey;
}
}

View File

@@ -44,8 +44,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
timeColorBubble = getColor(v.getContext(), R.color.briar_white);
// clone constraint sets from layout files
textConstraints
.clone(v.getContext(), R.layout.list_item_conversation_msg_in);
textConstraints.clone(v.getContext(),
R.layout.list_item_conversation_msg_in_content);
imageConstraints.clone(v.getContext(),
R.layout.list_item_conversation_msg_image);
imageTextConstraints.clone(v.getContext(),
@@ -61,8 +61,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
}
@Override
void bind(ConversationItem conversationItem) {
super.bind(conversationItem);
void bind(ConversationItem conversationItem, boolean selected) {
super.bind(conversationItem, selected);
ConversationMessageItem item =
(ConversationMessageItem) conversationItem;
if (item.getAttachments().isEmpty()) {

View File

@@ -28,9 +28,9 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder {
@Override
@CallSuper
void bind(ConversationItem item) {
void bind(ConversationItem item, boolean selected) {
ConversationNoticeItem notice = (ConversationNoticeItem) item;
super.bind(notice);
super.bind(notice, selected);
String text = notice.getMsgText();
if (isNullOrEmpty(text)) {

View File

@@ -26,9 +26,9 @@ class ConversationRequestViewHolder extends ConversationNoticeViewHolder {
}
@Override
void bind(ConversationItem item) {
void bind(ConversationItem item, boolean selected) {
ConversationRequestItem request = (ConversationRequestItem) item;
super.bind(request);
super.bind(request, selected);
if (request.wasAnswered() && request.canBeOpened()) {
acceptButton.setVisibility(VISIBLE);

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/msg_selected_background" android:state_activated="true" />
</selector>

View File

@@ -1,73 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout"
android:layout_width="wrap_content"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_margin_tail"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginTop="@dimen/message_bubble_margin"
android:layout_marginEnd="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:layout_marginBottom="@dimen/message_bubble_margin"
android:background="@drawable/msg_in"
android:elevation="@dimen/message_bubble_elevation">
android:background="@drawable/list_item_background_selectable">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/imageList"
android:layout_width="@dimen/message_bubble_image_default"
android:layout_height="@dimen/message_bubble_image_default"
android:orientation="vertical"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2"
tools:ignore="ContentDescription"
tools:listitem="@layout/list_item_image" />
<!--
We need to wrap the actual layout, because
* we want to clone the ConstraintLayout's constraints in the ViewHolder
* we want to have a selectable frame around the message bubble
-->
<include layout="@layout/list_item_conversation_msg_in_content" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/text"
style="@style/TextMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
android:layout_marginTop="@dimen/message_bubble_padding_top_inner"
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
android:textColor="?android:attr/textColorPrimary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageList"
tools:text="The text of a message which can sometimes be a bit longer as well" />
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text">
<TextView
android:id="@+id/time"
style="@style/TextMessage.Timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Dec 24, 13:37" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_margin_tail"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginTop="@dimen/message_bubble_margin"
android:layout_marginEnd="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:layout_marginBottom="@dimen/message_bubble_margin"
android:background="@drawable/msg_in"
android:elevation="@dimen/message_bubble_elevation">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/imageList"
android:layout_width="@dimen/message_bubble_image_default"
android:layout_height="@dimen/message_bubble_image_default"
android:orientation="vertical"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2"
tools:ignore="ContentDescription"
tools:listitem="@layout/list_item_image" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/text"
style="@style/TextMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
android:layout_marginTop="@dimen/message_bubble_padding_top_inner"
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
android:textColor="?android:attr/textColorPrimary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageList"
tools:text="The text of a message which can sometimes be a bit longer as well" />
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text">
<TextView
android:id="@+id/time"
style="@style/TextMessage.Timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Dec 24, 13:37" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!-- This is needed to right-align message bubble in RecyclerView -->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:background="@drawable/list_item_background_selectable">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout"

View File

@@ -4,8 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_margin"
android:orientation="vertical">
android:background="@drawable/list_item_background_selectable"
android:orientation="vertical"
android:paddingTop="@dimen/message_bubble_margin">
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/msgText"

View File

@@ -4,8 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_margin"
android:orientation="vertical">
android:background="@drawable/list_item_background_selectable"
android:orientation="vertical"
android:paddingTop="@dimen/message_bubble_margin">
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/msgText"

View File

@@ -4,8 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_margin"
android:orientation="vertical">
android:background="@drawable/list_item_background_selectable"
android:orientation="vertical"
android:paddingTop="@dimen/message_bubble_margin">
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/msgText"
@@ -19,7 +20,8 @@
android:background="@drawable/msg_in_top"
android:elevation="@dimen/message_bubble_elevation"
android:textColor="?android:attr/textColorPrimary"
tools:text="Short message" />
tools:text="Short message"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:icon="@drawable/action_delete_white"
android:title="@string/delete"
app:showAsAction="always"/>
</menu>

View File

@@ -42,6 +42,7 @@
<color name="msg_stroke_dark">#333333</color>
<color name="msg_stroke">@color/msg_stroke_light</color>
<color name="msg_status_bubble_background">#66000000</color>
<color name="msg_selected_background">@color/briar_accent</color>
<!-- text colors -->
<color name="briar_text_link">@color/briar_blue_light</color>

View File

@@ -139,7 +139,7 @@
<string name="dialog_title_delete_all_messages">Confirm Message Deletion</string>
<string name="dialog_message_delete_all_messages">Are you sure that you want to delete all messages?</string>
<string name="dialog_title_not_all_messages_deleted">Could not delete all messages</string>
<string name="dialog_message_not_all_messages_deleted">Messages related to ongoing introductions or invitations cannot be deleted until they conclude.</string>
<string name="dialog_message_not_all_messages_deleted">Messages related to\n\n• ongoing introductions\n• ongoing (blog/forum/group) invitations\n• partly downloaded messages\n\ncannot be deleted until they conclude.</string>
<string name="delete_contact">Delete contact</string>
<string name="dialog_title_delete_contact">Confirm Contact Deletion</string>
<string name="dialog_message_delete_contact">Are you sure that you want to remove this contact and all messages exchanged with this contact?</string>

View File

@@ -8,6 +8,7 @@
<item name="android:textColorLink">@color/briar_text_link</item>
<item name="android:windowBackground">@color/window_background</item>
<item name="android:windowAnimationStyle">@style/ActivityAnimation</item>
<item name="windowActionModeOverlay">true</item>
<item name="alertDialogTheme">@style/BriarDialogTheme.Neutral</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>

View File

@@ -36,6 +36,7 @@ dependencyVerification {
'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0:localbroadcastmanager-1.0.0.aar:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8',
'androidx.preference:preference:1.1.0:preference-1.1.0.aar:6cf1a099b03b3254041b04701841865b2708c0b546b9036c8b0dada0aa59de57',
'androidx.print:print:1.0.0:print-1.0.0.aar:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd',
'androidx.recyclerview:recyclerview-selection:1.0.0:recyclerview-selection-1.0.0.aar:db3db72af8cfcd701ab6ed7a080327a2e993e3a429f5efb8f0c108bff38c4922',
'androidx.recyclerview:recyclerview:1.1.0-beta04:recyclerview-1.1.0-beta04.aar:c3c8310eb058a660a845cf814a54f56e6f448b7855f9ccea2a5ad18189f57f69',
'androidx.savedstate:savedstate:1.0.0:savedstate-1.0.0.aar:2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83',
'androidx.test.espresso:espresso-contrib:3.2.0:espresso-contrib-3.2.0.aar:9e43811e5845e84f2964f0032fd50cd11dca3dc8d3b0703626dd12d50433bb35',

View File

@@ -47,6 +47,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -371,15 +372,20 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
public Set<MessageId> getMessageIds(Transaction txn, ContactId c)
throws DbException {
GroupId g = getContactGroup(db.getContact(txn, c)).getId();
BdfDictionary query = BdfDictionary.of(
new BdfEntry(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE));
Set<MessageId> result = new HashSet<>();
try {
Map<MessageId, BdfDictionary> results =
clientHelper.getMessageMetadataAsDictionary(txn, g, query);
return results.keySet();
Map<MessageId, BdfDictionary> messages =
clientHelper.getMessageMetadataAsDictionary(txn, g);
for (Map.Entry<MessageId, BdfDictionary> entry : messages
.entrySet()) {
Long type = entry.getValue().getOptionalLong(MSG_KEY_MSG_TYPE);
if (type == null || type == PRIVATE_MESSAGE)
result.add(entry.getKey());
}
} catch (FormatException e) {
throw new DbException(e);
}
return result;
}
@Override
@@ -450,7 +456,8 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
Long messageType = meta.getOptionalLong(MSG_KEY_MSG_TYPE);
if (messageType != null && messageType != PRIVATE_MESSAGE)
throw new AssertionError("not supported");
headers = parseAttachmentHeaders(meta);
headers = messageType == null ? emptyList() :
parseAttachmentHeaders(meta);
} catch (FormatException e) {
throw new DbException(e);
}

View File

@@ -208,6 +208,32 @@ public class MessagingManagerIntegrationTest
assertGroupCounts(c0, 0, 0);
}
@Test
public void testDeleteLegacySubset() throws Exception {
// send legacy message
GroupId g = c0.getMessagingManager().getConversationId(contactId);
PrivateMessage m0 = messageFactory.createLegacyPrivateMessage(g,
clock.currentTimeMillis(), getRandomString(42));
c0.getMessagingManager().addLocalMessage(m0);
syncMessage(c0, c1, contactId, 1, true);
// message arrived on both sides
assertEquals(1, getMessages(c0).size());
assertEquals(1, getMessages(c1).size());
// delete message on both sides (deletes all, because returns true)
Set<MessageId> toDelete = new HashSet<>();
toDelete.add(m0.getMessage().getId());
assertTrue(c0.getConversationManager()
.deleteMessages(contactId, toDelete));
assertTrue(c1.getConversationManager()
.deleteMessages(contactId, toDelete));
// message was deleted
assertEquals(0, getMessages(c0).size());
assertEquals(0, getMessages(c1).size());
}
@Test
public void testDeleteAttachment() throws Exception {
// send one message with attachment

View File

@@ -16,6 +16,7 @@ import org.briarproject.briar.api.blog.BlogFactory;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.forum.ForumManager;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.introduction.IntroductionManager;
@@ -89,6 +90,8 @@ public interface BriarIntegrationTestComponent
ContactManager getContactManager();
ConversationManager getConversationManager();
DatabaseComponent getDatabaseComponent();
BlogManager getBlogManager();