diff --git a/build.gradle b/build.gradle
index 72d2cbe280b9231cf530e76574e1531b527e17ed..79c4737acb03a5bfed8adcf642c1de5a634333c9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -98,6 +98,7 @@ dependencies {
implementation 'com.github.ipld:java-cid:v1.3.1'
implementation 'com.splitwise:tokenautocomplete:3.0.2'
implementation 'me.saket:better-link-movement-method:2.2.0'
+ implementation 'com.github.singpolyma:android-identicons:master-SNAPSHOT'
implementation 'org.snikket:webrtc-android:107.0.0'
// INSERT
}
diff --git a/src/cheogram/res/values/strings.xml b/src/cheogram/res/values/strings.xml
index ed2152ab9b54fdf514f1432163029b5e24ec2f41..d4bc883598daf9b0b2d61f4d067b5ab49140155c 100644
--- a/src/cheogram/res/values/strings.xml
+++ b/src/cheogram/res/values/strings.xml
@@ -25,6 +25,7 @@
Go
OLED Black
Invite to Chat
+ Show only this thread
Use Phone Accounts for Incoming Calls
Incoming calls from phone numbers may ring with your system dialler instead of this app\'s notification settings
diff --git a/src/cheogram/res/values/themes.xml b/src/cheogram/res/values/themes.xml
index c7d21f641b9163938934e6958c73c2f4317a9810..55494cea1a2b4a4b56220eeabb5fe28ea84b7f18 100644
--- a/src/cheogram/res/values/themes.xml
+++ b/src/cheogram/res/values/themes.xml
@@ -121,6 +121,7 @@
- @drawable/ic_help_white_24dp
- @drawable/ic_question_answer_white_24dp
- @drawable/ic_lock_open_white_24dp
+ - @drawable/ic_lock_black_18dp
- @drawable/ic_settings_black_24dp
- @drawable/ic_share_white_24dp
- @drawable/ic_cloud_download_white_24dp
@@ -275,6 +276,7 @@
- @drawable/ic_help_white_24dp
- @drawable/ic_question_answer_white_24dp
- @drawable/ic_lock_open_white_24dp
+ - @drawable/ic_lock_white_18dp
- @drawable/ic_settings_white_24dp
- @drawable/ic_share_white_24dp
- @drawable/ic_cloud_download_white_24dp
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index b9bdd21b2d2ecc5721b15ce66b1dfcf492904389..e7e098815eaf1c1541c634fb11f945770588f2e4 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -148,6 +148,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
private String mFirstMamReference = null;
protected int mCurrentTab = -1;
protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
+ protected Element thread = null;
+ protected boolean lockThread = false;
+ protected boolean userSelectedThread = false;
public Conversation(final String name, final Account account, final Jid contactJid,
final int mode) {
@@ -529,7 +532,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
messages.addAll(this.messages);
}
for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) {
- if (iterator.next().wasMergedIntoPrevious()) {
+ Message m = iterator.next();
+ if (m.wasMergedIntoPrevious() || (getLockThread() && (m.getThread() == null || !m.getThread().getContent().equals(getThread().getContent())))) {
iterator.remove();
}
}
@@ -628,6 +632,31 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
this.draftMessage = draftMessage;
}
+ public Element getThread() {
+ return this.thread;
+ }
+
+ public void setThread(Element thread) {
+ this.thread = thread;
+ }
+
+ public void setLockThread(boolean flag) {
+ this.lockThread = flag;
+ if (flag) setUserSelectedThread(true);
+ }
+
+ public boolean getLockThread() {
+ return this.lockThread;
+ }
+
+ public void setUserSelectedThread(boolean flag) {
+ this.userSelectedThread = flag;
+ }
+
+ public boolean getUserSelectedThread() {
+ return this.userSelectedThread;
+ }
+
public boolean isRead() {
synchronized (this.messages) {
for(final Message message : Lists.reverse(this.messages)) {
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index e88bf3265506aa2815da29217b4e3c7903e8de5f..47fde1f5051683508e46a1bc2394df2730acf528 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -418,6 +418,23 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.subject = subject;
}
+ public Element getThread() {
+ if (this.payloads == null) return null;
+
+ for (Element el : this.payloads) {
+ if (el.getName().equals("thread") && el.getNamespace().equals("jabber:client")) {
+ return el;
+ }
+ }
+
+ return null;
+ }
+
+ public void setThread(Element thread) {
+ payloads.removeIf(el -> el.getName().equals("thread") && el.getNamespace().equals("jabber:client"));
+ addPayload(thread);
+ }
+
public void setMucUser(MucOptions.User user) {
this.user = new WeakReference<>(user);
}
@@ -907,9 +924,15 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public void addPayload(Element el) {
+ if (el == null) return;
+
this.payloads.add(el);
}
+ public List getPayloads() {
+ return new ArrayList<>(this.payloads);
+ }
+
public Element getHtml() {
if (this.payloads == null) return null;
@@ -920,7 +943,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
return null;
- }
+ }
public List getCommands() {
if (this.payloads == null) return null;
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
index 4b055e15883131ab61afff17482c7b4dbdec13d3..90a312204094c58cc2bda1baf3e27ed4aa7f25bf 100644
--- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -62,6 +62,9 @@ public class MessageGenerator extends AbstractGenerator {
if (message.edited()) {
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
}
+ for (Element el : message.getPayloads()) {
+ packet.addChild(el);
+ }
return packet;
}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 952d3d83efada6f51e6a5cb657c42589e0555e23..a6f924b28cb7e903f872c88285dfb18395fc14e0 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -610,6 +610,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
message.addPayload(el);
}
+ if (el.getName().equals("thread") && (el.getNamespace() == null || el.getNamespace().equals("jabber:client"))) {
+ el.setAttribute("xmlns", "jabber:client");
+ message.addPayload(el);
+ }
}
if (conversationMultiMode) {
message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 6091a59a61161d93429ca3805414ebfac513dc13..7eac0d2285d01bef1dc06c879e4ffc00562e99d0 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -568,6 +568,7 @@ public class XmppConnectionService extends Service {
encryption = Message.ENCRYPTION_DECRYPTED;
}
Message message = new Message(conversation, uri.toString(), encryption);
+ message.setThread(conversation.getThread());
Message.configurePrivateMessage(message);
if (encryption == Message.ENCRYPTION_DECRYPTED) {
getPgpEngine().encrypt(message, callback);
@@ -584,6 +585,7 @@ public class XmppConnectionService extends Service {
} else {
message = new Message(conversation, "", conversation.getNextEncryption());
}
+ message.setThread(conversation.getThread());
if (!Message.configurePrivateFileMessage(message)) {
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_FILE);
@@ -616,6 +618,7 @@ public class XmppConnectionService extends Service {
} else {
message = new Message(conversation, "", conversation.getNextEncryption());
}
+ message.setThread(conversation.getThread());
if (!Message.configurePrivateFileMessage(message)) {
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE);
@@ -980,6 +983,7 @@ public class XmppConnectionService extends Service {
private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
final Message message = new Message(conversation, body, conversation.getNextEncryption());
+ if (inReplyTo != null) message.setThread(inReplyTo.getThread());
if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 42e095d786da861f10e8466c08803d036c699da1..123f154b3456ea7b9011facb49e5c4f040422313 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -264,6 +264,7 @@ public class ConversationFragment extends XmppFragment
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) {
+ updateThreadFromLastMessage();
fireReadEvent();
}
}
@@ -854,6 +855,7 @@ public class ConversationFragment extends XmppFragment
}
private void sendMessage() {
+ conversation.setUserSelectedThread(false);
if (mediaPreviewAdapter.hasAttachments()) {
commitAttachments();
return;
@@ -870,6 +872,7 @@ public class ConversationFragment extends XmppFragment
final Message message;
if (conversation.getCorrectingMessage() == null) {
message = new Message(conversation, body, conversation.getNextEncryption());
+ message.setThread(conversation.getThread());
Message.configurePrivateMessage(message);
} else {
message = conversation.getCorrectingMessage();
@@ -953,6 +956,8 @@ public class ConversationFragment extends XmppFragment
this.binding.textinput.setHint(UIHelper.getMessageHint(getActivity(), conversation));
getActivity().invalidateOptionsMenu();
}
+
+ binding.messagesView.post(this::updateThreadFromLastMessage);
}
public void setupIme() {
@@ -1248,6 +1253,33 @@ public class ConversationFragment extends XmppFragment
new EditMessageActionModeCallback(this.binding.textinput));
}
+ messageListAdapter.setOnMessageBoxClicked(message -> {
+ setThread(message.getThread());
+ conversation.setUserSelectedThread(true);
+ });
+
+ binding.threadIdenticonLayout.setOnClickListener(v -> {
+ boolean wasLocked = conversation.getLockThread();
+ conversation.setLockThread(false);
+ if (wasLocked) {
+ conversation.setUserSelectedThread(false);
+ refresh();
+ updateThreadFromLastMessage();
+ } else {
+ newThread();
+ conversation.setUserSelectedThread(true);
+ }
+ });
+
+ binding.threadIdenticonLayout.setOnLongClickListener(v -> {
+ boolean wasLocked = conversation.getLockThread();
+ conversation.setLockThread(false);
+ setThread(null);
+ conversation.setUserSelectedThread(true);
+ if (wasLocked) refresh();
+ return true;
+ });
+
return binding.getRoot();
}
@@ -1275,9 +1307,25 @@ public class ConversationFragment extends XmppFragment
}
private void quoteMessage(Message message) {
+ setThread(message.getThread());
+ conversation.setUserSelectedThread(true);
quoteText(MessageUtils.prepareQuote(message));
}
+ private void setThread(Element thread) {
+ this.conversation.setThread(thread);
+ binding.threadIdenticon.setAlpha(0f);
+ binding.threadIdenticonLock.setVisibility(this.conversation.getLockThread() ? View.VISIBLE : View.GONE);
+ if (thread != null) {
+ final String threadId = thread.getContent();
+ if (threadId != null) {
+ binding.threadIdenticon.setAlpha(1f);
+ binding.threadIdenticon.setColor(UIHelper.getColorForName(threadId));
+ binding.threadIdenticon.setHash(UIHelper.identiconHash(threadId));
+ }
+ }
+ }
+
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
// This should cancel any remaining click events that would otherwise trigger links
@@ -1327,6 +1375,7 @@ public class ConversationFragment extends XmppFragment
MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
MenuItem correctMessage = menu.findItem(R.id.correct_message);
MenuItem retractMessage = menu.findItem(R.id.retract_message);
+ MenuItem onlyThisThread = menu.findItem(R.id.only_this_thread);
MenuItem shareWith = menu.findItem(R.id.share_with);
MenuItem sendAgain = menu.findItem(R.id.send_again);
MenuItem copyUrl = menu.findItem(R.id.copy_url);
@@ -1334,6 +1383,7 @@ public class ConversationFragment extends XmppFragment
MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
MenuItem deleteFile = menu.findItem(R.id.delete_file);
MenuItem showErrorMessage = menu.findItem(R.id.show_error_message);
+ onlyThisThread.setVisible(!conversation.getLockThread() && m.getThread() != null);
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(m);
final boolean showError =
m.getStatus() == Message.STATUS_SEND_FAILED
@@ -1470,6 +1520,11 @@ public class ConversationFragment extends XmppFragment
case R.id.open_with:
openWith(selectedMessage);
return true;
+ case R.id.only_this_thread:
+ conversation.setLockThread(true);
+ setThread(selectedMessage.getThread());
+ refresh();
+ return true;
default:
return super.onContextItemSelected(item);
}
@@ -2116,7 +2171,29 @@ public class ConversationFragment extends XmppFragment
}
}
+ private void newThread() {
+ Element thread = new Element("thread", "jabber:client");
+ thread.setContent(UUID.randomUUID().toString());
+ setThread(thread);
+ }
+
+ private void updateThreadFromLastMessage() {
+ if (this.conversation != null && !this.conversation.getUserSelectedThread() && TextUtils.isEmpty(binding.textinput.getText())) {
+ Message message = getLastVisibleMessage();
+ if (message == null) {
+ newThread();
+ } else {
+ setThread(message.getThread());
+ }
+ }
+ }
+
private String getLastVisibleMessageUuid() {
+ Message message = getLastVisibleMessage();
+ return message == null ? null : message.getUuid();
+ }
+
+ private Message getLastVisibleMessage() {
if (binding == null) {
return null;
}
@@ -2140,7 +2217,7 @@ public class ConversationFragment extends XmppFragment
while (message.next() != null && message.next().wasMergedIntoPrevious()) {
message = message.next();
}
- return message.getUuid();
+ return message;
}
}
}
@@ -3684,13 +3761,9 @@ public class ConversationFragment extends XmppFragment
@Override
public void onContactPictureClicked(Message message) {
- String fingerprint;
- if (message.getEncryption() == Message.ENCRYPTION_PGP
- || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
- fingerprint = "pgp";
- } else {
- fingerprint = message.getFingerprint();
- }
+ setThread(message.getThread());
+ conversation.setUserSelectedThread(true);
+
final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
if (received) {
if (message.getConversation() instanceof Conversation
@@ -3724,15 +3797,8 @@ public class ConversationFragment extends XmppFragment
.show();
}
}
- return;
- } else {
- if (!message.getContact().isSelf()) {
- activity.switchToContactDetails(message.getContact(), fingerprint);
- return;
- }
}
}
- activity.switchToAccount(message.getConversation().getAccount(), fingerprint);
}
private Activity requireActivity() {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
index 7e0897b271e1004ec7b47a3c1a8c701e7a8c6a96..1e44e08373324f97beea339a7e92eb8fd1d048b8 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -42,6 +42,8 @@ import com.cheogram.android.BobTransfer;
import com.google.common.base.Strings;
+import com.lelloman.identicon.view.GithubIdenticonView;
+
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -107,6 +109,7 @@ public class MessageAdapter extends ArrayAdapter {
private List highlightedTerm = null;
private final DisplayMetrics metrics;
private OnContactPictureClicked mOnContactPictureClickedListener;
+ private OnContactPictureClicked mOnMessageBoxClickedListener;
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
private boolean mUseGreenBackground = false;
private final boolean mForceNames;
@@ -146,6 +149,10 @@ public class MessageAdapter extends ArrayAdapter {
this.mOnContactPictureClickedListener = listener;
}
+ public void setOnMessageBoxClicked(OnContactPictureClicked listener) {
+ this.mOnMessageBoxClickedListener = listener;
+ }
+
public Activity getActivity() {
return activity;
}
@@ -717,6 +724,7 @@ public class MessageAdapter extends ArrayAdapter {
viewHolder.subject = view.findViewById(R.id.message_subject);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
+ viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
break;
case RECEIVED:
view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false);
@@ -733,6 +741,7 @@ public class MessageAdapter extends ArrayAdapter {
viewHolder.encryption = view.findViewById(R.id.message_encryption);
viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
viewHolder.commands_list = view.findViewById(R.id.commands_list);
+ viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
break;
case STATUS:
view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
@@ -751,6 +760,19 @@ public class MessageAdapter extends ArrayAdapter {
}
}
+ if (viewHolder.thread_identicon != null) {
+ viewHolder.thread_identicon.setVisibility(View.GONE);
+ final Element thread = message.getThread();
+ if (thread != null) {
+ final String threadId = thread.getContent();
+ if (threadId != null) {
+ viewHolder.thread_identicon.setVisibility(View.VISIBLE);
+ viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId));
+ viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId));
+ }
+ }
+ }
+
boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme();
if (type == DATE_SEPARATOR) {
@@ -822,6 +844,18 @@ public class MessageAdapter extends ArrayAdapter {
resetClickListener(viewHolder.message_box, viewHolder.messageBody);
+ viewHolder.message_box.setOnClickListener(v -> {
+ if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
+ MessageAdapter.this.mOnMessageBoxClickedListener
+ .onContactPictureClicked(message);
+ }
+ });
+ viewHolder.messageBody.setOnClickListener(v -> {
+ if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
+ MessageAdapter.this.mOnMessageBoxClickedListener
+ .onContactPictureClicked(message);
+ }
+ });
viewHolder.contact_picture.setOnClickListener(v -> {
if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
MessageAdapter.this.mOnContactPictureClickedListener
@@ -1028,6 +1062,7 @@ public class MessageAdapter extends ArrayAdapter {
protected TextView status_message;
protected TextView encryption;
protected ListView commands_list;
+ protected GithubIdenticonView thread_identicon;
}
class ThumbnailTask extends AsyncTask {
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index 7692138be38c01d2268ee43e7626cdae00719e86..2bbcd08b7c5094da14ad123e9d0ce05bd850c593 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -11,8 +11,10 @@ import androidx.annotation.ColorInt;
import androidx.core.content.res.ResourcesCompat;
import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Calendar;
@@ -229,6 +231,16 @@ public class UIHelper {
}
}
+ public static int identiconHash(String name) {
+ try {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ byte[] digest = sha1.digest(name.getBytes(StandardCharsets.UTF_8));
+ return Ints.fromByteArray(digest);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
public static int getColorForName(String name) {
return getColorForName(name, false);
}
diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml
index 914ee1950002312e4dcfbeda64954244f17737e1..3457b68bb5311044d55c9ea709666046584f4f2f 100644
--- a/src/main/res/layout/account_row.xml
+++ b/src/main/res/layout/account_row.xml
@@ -22,6 +22,7 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
+ android:layout_toEndOf="@+id/account_image"
android:layout_toRightOf="@+id/account_image"
android:orientation="vertical"
android:paddingLeft="@dimen/avatar_item_distance"
diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml
index 05915e0c7e88662368fdfdf60d2f635d6acae685..477ecf3f012a51b675de67ac4593e09424bc243b 100644
--- a/src/main/res/layout/fragment_conversation.xml
+++ b/src/main/res/layout/fragment_conversation.xml
@@ -60,10 +60,10 @@
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+ android:layout_alignParentBottom="true">
+
+
+
+
+
+
+
+
+
+
+
+
+