aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Gultsch <daniel@gultsch.de>2017-11-19 01:53:04 +0100
committerDaniel Gultsch <daniel@gultsch.de>2017-11-20 11:26:20 +0100
commit28e005f926294bbb3f8c7621fb1484fa06b1ce9a (patch)
tree96709297967884c785defaae72c6e9a5050181bf
parent1c65a17ff1a44a5c7d50b77fcbba54ec3d0700a0 (diff)
send and show read markers in private, non-anonymous groups
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java2
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java11
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java49
-rw-r--r--src/main/java/eu/siacs/conversations/entities/MucOptions.java34
-rw-r--r--src/main/java/eu/siacs/conversations/entities/ReadByMarker.java166
-rw-r--r--src/main/java/eu/siacs/conversations/generator/MessageGenerator.java11
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java21
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java7
-rw-r--r--src/main/java/eu/siacs/conversations/services/AvatarService.java84
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java6
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java57
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java14
-rw-r--r--src/main/java/eu/siacs/conversations/utils/UIHelper.java71
-rw-r--r--src/main/res/values/strings.xml1
14 files changed, 481 insertions, 53 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 7dc8c9f..511af46 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -82,7 +82,7 @@ public final class Config {
public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY;
public static final boolean REMOVE_BROKEN_DEVICES = false;
public static final boolean OMEMO_PADDING = false;
- public static boolean PUT_AUTH_TAG_INTO_KEY = true;
+ public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 28b51b9..44aea69 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -289,6 +289,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null;
}
+ public Message findMessageWithRemoteId(String id) {
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid())) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
public boolean hasMessageWithCounterpart(Jid counterpart) {
synchronized (this.messages) {
for(Message message : 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 d01ec2d..dcddb3e 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -3,9 +3,17 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
import android.text.SpannableStringBuilder;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -62,6 +70,7 @@ public class Message extends AbstractEntity {
public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String READ = "read";
public static final String ERROR_MESSAGE = "errorMsg";
+ public static final String READ_BY_MARKERS = "readByMarkers";
public static final String ME_COMMAND = "/me ";
@@ -88,11 +97,13 @@ public class Message extends AbstractEntity {
private Message mPreviousMessage = null;
private String axolotlFingerprint = null;
private String errorMessage = null;
+ protected Set<ReadByMarker> readByMarkers = new HashSet<>();
private Boolean isGeoUri = null;
private Boolean isEmojisOnly = null;
private Boolean treatAsDownloadable = null;
private FileParams fileParams = null;
+ private List<MucOptions.User> counterparts;
private Message(Conversation conversation) {
this.conversation = conversation;
@@ -120,6 +131,7 @@ public class Message extends AbstractEntity {
true,
null,
false,
+ null,
null);
}
@@ -128,7 +140,7 @@ public class Message extends AbstractEntity {
final int encryption, final int status, final int type, final boolean carbon,
final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint, final boolean read,
- final String edited, final boolean oob, final String errorMessage) {
+ final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers) {
this.conversation = conversation;
this.uuid = uuid;
this.conversationUuid = conversationUUid;
@@ -148,6 +160,7 @@ public class Message extends AbstractEntity {
this.edited = edited;
this.oob = oob;
this.errorMessage = errorMessage;
+ this.readByMarkers = new HashSet<>();
}
public static Message fromCursor(Cursor cursor, Conversation conversation) {
@@ -193,7 +206,8 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(READ)) > 0,
cursor.getString(cursor.getColumnIndex(EDITED)),
cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
- cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)));
+ cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
+ ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@@ -248,6 +262,7 @@ public class Message extends AbstractEntity {
values.put(EDITED, edited);
values.put(OOB, oob ? 1 : 0);
values.put(ERROR_MESSAGE,errorMessage);
+ values.put(READ_BY_MARKERS,ReadByMarker.toJson(readByMarkers).toString());
return values;
}
@@ -415,6 +430,25 @@ public class Message extends AbstractEntity {
this.transferable = transferable;
}
+ public boolean addReadByMarker(ReadByMarker readByMarker) {
+ if (readByMarker.getRealJid() != null) {
+ if (readByMarker.getRealJid().toBareJid().equals(trueCounterpart)) {
+ Log.d(Config.LOGTAG,"trying to add read marker by "+readByMarker.getRealJid()+" to "+body);
+ return false;
+ }
+ } else if (readByMarker.getFullJid() != null) {
+ if (readByMarker.getFullJid().equals(counterpart)) {
+ Log.d(Config.LOGTAG,"trying to add read marker by "+readByMarker.getFullJid()+" to "+body);
+ return false;
+ }
+ }
+ return this.readByMarkers.add(readByMarker);
+ }
+
+ public Set<ReadByMarker> getReadByMarkers() {
+ return Collections.unmodifiableSet(this.readByMarkers);
+ }
+
public boolean similar(Message message) {
if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
return this.serverMsgId.equals(message.getServerMsgId());
@@ -515,7 +549,8 @@ public class Message extends AbstractEntity {
!this.bodyIsOnlyEmojis() &&
!message.bodyIsOnlyEmojis() &&
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
- UIHelper.sameDay(message.getTimeSent(),this.getTimeSent())
+ UIHelper.sameDay(message.getTimeSent(),this.getTimeSent()) &&
+ this.getReadByMarkers().equals(message.getReadByMarkers())
);
}
@@ -529,6 +564,14 @@ public class Message extends AbstractEntity {
);
}
+ public void setCounterparts(List<MucOptions.User> counterparts) {
+ this.counterparts = counterparts;
+ }
+
+ public List<MucOptions.User> getCounterparts() {
+ return this.counterparts;
+ }
+
public static class MergeSeparator {}
public SpannableStringBuilder getMergedBody() {
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
index 7c66878..8414fd9 100644
--- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -11,6 +11,7 @@ import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.utils.JidHelper;
+import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
@@ -280,6 +281,10 @@ public class MucOptions {
return options.getAccount();
}
+ public Conversation getConversation() {
+ return options.getConversation();
+ }
+
public Jid getFullJid() {
return fullJid;
}
@@ -521,6 +526,21 @@ public class MucOptions {
return null;
}
+ public User findUser(ReadByMarker readByMarker) {
+ if (readByMarker.getRealJid() != null) {
+ User user = findUserByRealJid(readByMarker.getRealJid().toBareJid());
+ if (user == null) {
+ user = new User(this,readByMarker.getFullJid());
+ user.setRealJid(readByMarker.getRealJid());
+ }
+ return user;
+ } else if (readByMarker.getFullJid() != null) {
+ return findUserByFullJid(readByMarker.getFullJid());
+ } else {
+ return null;
+ }
+ }
+
public boolean isContactInRoom(Contact contact) {
return findUserByRealJid(contact.getJid().toBareJid()) != null;
}
@@ -655,17 +675,9 @@ public class MucOptions {
if (builder.length() != 0) {
builder.append(", ");
}
- Contact contact = user.getContact();
- if (contact != null && !contact.getDisplayName().isEmpty()) {
- builder.append(contact.getDisplayName().split("\\s+")[0]);
- } else {
- final String name = user.getName();
- final Jid jid = user.getRealJid();
- if (name != null){
- builder.append(name.split("\\s+")[0]);
- } else if (jid != null) {
- builder.append(jid.getLocalpart());
- }
+ String name = UIHelper.getDisplayName(user);
+ if (name != null) {
+ builder.append(name.split("\\s+")[0]);
}
}
return builder.toString();
diff --git a/src/main/java/eu/siacs/conversations/entities/ReadByMarker.java b/src/main/java/eu/siacs/conversations/entities/ReadByMarker.java
new file mode 100644
index 0000000..6767212
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/ReadByMarker.java
@@ -0,0 +1,166 @@
+package eu.siacs.conversations.entities;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class ReadByMarker {
+
+ private ReadByMarker() {
+
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ReadByMarker marker = (ReadByMarker) o;
+
+ if (fullJid != null ? !fullJid.equals(marker.fullJid) : marker.fullJid != null)
+ return false;
+ return realJid != null ? realJid.equals(marker.realJid) : marker.realJid == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = fullJid != null ? fullJid.hashCode() : 0;
+ result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
+ return result;
+ }
+
+ private Jid fullJid;
+ private Jid realJid;
+
+ public Jid getFullJid() {
+ return fullJid;
+ }
+
+ public Jid getRealJid() {
+ return realJid;
+ }
+
+ public JSONObject toJson() {
+ JSONObject jsonObject = new JSONObject();
+ if (fullJid != null) {
+ try {
+ jsonObject.put("fullJid", fullJid.toPreppedString());
+ } catch (JSONException e) {
+ //ignore
+ }
+ }
+ if (realJid != null) {
+ try {
+ jsonObject.put("realJid", realJid.toPreppedString());
+ } catch (JSONException e) {
+ //ignore
+ }
+ }
+ return jsonObject;
+ }
+
+ public static Set<ReadByMarker> fromJson(JSONArray jsonArray) {
+ HashSet<ReadByMarker> readByMarkers = new HashSet<>();
+ for(int i = 0; i < jsonArray.length(); ++i) {
+ try {
+ readByMarkers.add(fromJson(jsonArray.getJSONObject(i)));
+ } catch (JSONException e) {
+ //ignored
+ }
+ }
+ return readByMarkers;
+ }
+
+ public static ReadByMarker from(Jid fullJid, Jid realJid) {
+ final ReadByMarker marker = new ReadByMarker();
+ marker.fullJid = fullJid;
+ marker.realJid = realJid;
+ return marker;
+ }
+
+ public static ReadByMarker from(Message message) {
+ final ReadByMarker marker = new ReadByMarker();
+ marker.fullJid = message.getCounterpart();
+ marker.realJid = message.getTrueCounterpart();
+ return marker;
+ }
+
+ public static ReadByMarker from(MucOptions.User user) {
+ final ReadByMarker marker = new ReadByMarker();
+ marker.fullJid = user.getFullJid();
+ marker.realJid = user.getRealJid();
+ return marker;
+ }
+
+ public static Set<ReadByMarker> from(Collection<MucOptions.User> users) {
+ final HashSet<ReadByMarker> markers = new HashSet<>();
+ for(MucOptions.User user : users) {
+ markers.add(from(user));
+ }
+ return markers;
+ }
+
+ public static ReadByMarker fromJson(JSONObject jsonObject) {
+ ReadByMarker marker = new ReadByMarker();
+ try {
+ marker.fullJid = Jid.fromString(jsonObject.getString("fullJid"),true);
+ } catch (JSONException | InvalidJidException e) {
+ marker.fullJid = null;
+ }
+ try {
+ marker.realJid = Jid.fromString(jsonObject.getString("realJid"),true);
+ } catch (JSONException | InvalidJidException e) {
+ marker.realJid = null;
+ }
+ return marker;
+ }
+
+ public static Set<ReadByMarker> fromJsonString(String json) {
+ try {
+ return fromJson(new JSONArray(json));
+ } catch (JSONException | NullPointerException e) {
+ return new HashSet<>();
+ }
+ }
+
+ public static JSONArray toJson(Set<ReadByMarker> readByMarkers) {
+ JSONArray jsonArray = new JSONArray();
+ for(ReadByMarker marker : readByMarkers) {
+ jsonArray.put(marker.toJson());
+ }
+ return jsonArray;
+ }
+
+ public static boolean contains(ReadByMarker needle, Set<ReadByMarker> readByMarkers) {
+ for(ReadByMarker marker : readByMarkers) {
+ if (marker.realJid != null && needle.realJid != null) {
+ if (marker.realJid.toBareJid().equals(needle.realJid.toBareJid())) {
+ return true;
+ }
+ } else if (marker.fullJid != null && needle.fullJid != null) {
+ if (marker.fullJid.equals(needle.fullJid)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean allUsersRepresented(Collection<MucOptions.User> users, Set<ReadByMarker> markers) {
+ for(MucOptions.User user : users) {
+ if (!contains(from(user),markers)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
index ec91cac..49355b1 100644
--- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -39,7 +39,6 @@ public class MessageGenerator extends AbstractGenerator {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
packet.setTo(message.getCounterpart());
packet.setType(MessagePacket.TYPE_CHAT);
- packet.addChild("markable", "urn:xmpp:chat-markers:0");
if (this.mXmppConnectionService.indicateReceived()) {
packet.addChild("request", "urn:xmpp:receipts");
}
@@ -54,6 +53,10 @@ public class MessageGenerator extends AbstractGenerator {
packet.setTo(message.getCounterpart().toBareJid());
packet.setType(MessagePacket.TYPE_GROUPCHAT);
}
+ if (conversation.getMode() == Conversation.MODE_SINGLE ||
+ (conversation.getMucOptions().nonanonymous() && conversation.getMucOptions().membersOnly() && message.getType() != Message.TYPE_PRIVATE)) {
+ packet.addChild("markable", "urn:xmpp:chat-markers:0");
+ }
packet.setFrom(account.getJid());
packet.setId(message.getUuid());
packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id",message.getUuid());
@@ -170,10 +173,10 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
- public MessagePacket confirm(final Account account, final Jid to, final String id) {
+ public MessagePacket confirm(final Account account, final Jid to, final String id, final boolean groupChat) {
MessagePacket packet = new MessagePacket();
- packet.setType(MessagePacket.TYPE_CHAT);
- packet.setTo(to);
+ packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
+ packet.setTo(groupChat ? to.toBareJid() : to);
packet.setFrom(account.getJid());
Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0");
received.setAttribute("id", id);
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index d501bb9..8deae03 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -29,6 +29,7 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.MessageArchiveService;
@@ -700,13 +701,29 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
if (displayed != null) {
+ final String id = displayed.getAttribute("id");
if (packet.fromAccount(account)) {
- Conversation conversation = mXmppConnectionService.find(account,counterpart.toBareJid());
+ Conversation conversation = mXmppConnectionService.find(account, counterpart.toBareJid());
if (conversation != null && (query == null || query.isCatchup())) {
mXmppConnectionService.markRead(conversation);
}
+ } else if (isTypeGroupChat) {
+ Conversation conversation = mXmppConnectionService.find(account, counterpart.toBareJid());
+ if (conversation != null && id != null) {
+ Message message = conversation.findMessageWithRemoteId(id);
+ if (message != null) {
+ final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
+ Jid trueJid = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
+ ReadByMarker readByMarker = ReadByMarker.from(counterpart,trueJid);
+ if (!conversation.getMucOptions().isSelf(counterpart) && message.addReadByMarker(readByMarker)) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": added read by ("+readByMarker.getRealJid()+") to message '"+message.getBody()+"'");
+ mXmppConnectionService.updateMessage(message);
+ }
+ }
+
+ }
} else {
- final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), displayed.getAttribute("id"), Message.STATUS_SEND_DISPLAYED);
+ final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), id, Message.STATUS_SEND_DISPLAYED);
Message message = displayedMessage == null ? null : displayedMessage.prev();
while (message != null
&& message.getStatus() == Message.STATUS_SEND_RECEIVED
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index 34d6260..de8dbb0 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -60,7 +60,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
- private static final int DATABASE_VERSION = 36;
+ private static final int DATABASE_VERSION = 37;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -197,6 +197,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.READ + " NUMBER DEFAULT 1, "
+ Message.OOB + " INTEGER, "
+ Message.ERROR_MESSAGE + " TEXT,"
+ + Message.READ_BY_MARKERS + " TEXT,"
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
@@ -454,6 +455,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ "=?", new String[]{account.getUuid()});
}
}
+
+ if (oldVersion < 37 && newVersion >= 37) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ_BY_MARKERS + " TEXTs");
+ }
}
private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java
index c7f97cd..cc517a8 100644
--- a/src/main/java/eu/siacs/conversations/services/AvatarService.java
+++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -10,10 +10,14 @@ import android.graphics.Typeface;
import android.net.Uri;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.LruCache;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
@@ -39,6 +43,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
private static final String PREFIX_GENERIC = "generic";
final private ArrayList<Integer> sizes = new ArrayList<>();
+ final private HashMap<String,Set<String>> conversationDependentKeys = new HashMap<>();
protected XmppConnectionService mXmppConnectionService = null;
@@ -184,6 +189,17 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
clear(conversation.getContact());
} else {
clear(conversation.getMucOptions());
+ synchronized (this.conversationDependentKeys) {
+ Set<String> keys = this.conversationDependentKeys.get(conversation.getUuid());
+ if (keys == null) {
+ return;
+ }
+ LruCache<String, Bitmap> cache = this.mXmppConnectionService.getBitmapCache();
+ for(String key : keys) {
+ cache.remove(key);
+ }
+ keys.clear();
+ }
}
}
@@ -194,17 +210,36 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return bitmap;
}
final List<MucOptions.User> users = mucOptions.getUsers(5);
+ if (users.size() == 0) {
+ bitmap = getImpl(mucOptions.getConversation().getName(),size);
+ } else {
+ bitmap = getImpl(users,size);
+ }
+ this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+ return bitmap;
+ }
+
+ private Bitmap get(List<MucOptions.User> users, int size, boolean cachedOnly) {
+ final String KEY = key(users, size);
+ Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
+ if (bitmap != null || cachedOnly) {
+ return bitmap;
+ }
+ bitmap = getImpl(users, size);
+ this.mXmppConnectionService.getBitmapCache().put(KEY,bitmap);
+ return bitmap;
+ }
+
+ private Bitmap getImpl(List<MucOptions.User> users, int size) {
int count = users.size();
- bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
bitmap.eraseColor(TRANSPARENT);
-
if (count == 0) {
- String name = mucOptions.getConversation().getName();
- drawTile(canvas, name, 0, 0, size, size);
+ throw new AssertionError("Unable to draw tiles for 0 users");
} else if (count == 1) {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
- drawTile(canvas, mucOptions.getConversation().getAccount(), size / 2 + 1, 0, size, size);
+ drawTile(canvas, users.get(0).getAccount(), size / 2 + 1, 0, size, size);
} else if (count == 2) {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
@@ -226,7 +261,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1,
size, size);
}
- this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
return bitmap;
}
@@ -248,6 +282,31 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
+ "_" + String.valueOf(size);
}
+ private String key(List<MucOptions.User> users, int size) {
+ final Conversation conversation = users.get(0).getConversation();
+ StringBuilder builder = new StringBuilder("TILE_");
+ builder.append(conversation.getUuid());
+
+ for(MucOptions.User user : users) {
+ builder.append("\0");
+ builder.append(user.getRealJid() == null ? "" : user.getRealJid().toBareJid().toPreppedString());
+ builder.append("\0");
+ builder.append(user.getFullJid() == null ? "" : user.getFullJid().toPreppedString());
+ }
+ final String key = builder.toString();
+ synchronized (this.conversationDependentKeys) {
+ Set<String> keys;
+ if (this.conversationDependentKeys.containsKey(conversation.getUuid())) {
+ keys = this.conversationDependentKeys.get(conversation.getUuid());
+ } else {
+ keys = new HashSet<>();
+ this.conversationDependentKeys.put(conversation.getUuid(),keys);
+ }
+ keys.add(key);
+ }
+ return key;
+ }
+
public Bitmap get(Account account, int size) {
return get(account, size, false);
}
@@ -268,7 +327,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
public Bitmap get(Message message, int size, boolean cachedOnly) {
final Conversation conversation = message.getConversation();
- if (message.getStatus() == Message.STATUS_RECEIVED) {
+ if (message.getType() == Message.TYPE_STATUS && message.getCounterparts() != null && message.getCounterparts().size() > 1) {
+ return get(message.getCounterparts(),size,cachedOnly);
+ } else if (message.getStatus() == Message.STATUS_RECEIVED) {
Contact c = message.getContact();
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
return get(c, size, cachedOnly);
@@ -320,11 +381,16 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (bitmap != null || cachedOnly) {
return bitmap;
}
- bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ bitmap = getImpl(name, size);
+ mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+ return bitmap;
+ }
+
+ private Bitmap getImpl(final String name, final int size) {
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
final String trimmedName = name == null ? "" : name.trim();
drawTile(canvas, trimmedName, 0, 0, size, size);
- mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
return bitmap;
}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index b9fa574..74472fa 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -3394,11 +3394,13 @@ public class XmppConnectionService extends Service {
if (confirmMessages()
&& markable != null
&& markable.trusted()
- && markable.getRemoteMsgId() != null) {
+ && markable.getRemoteMsgId() != null
+ && markable.getType() != Message.TYPE_PRIVATE) {
Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
Account account = conversation.getAccount();
final Jid to = markable.getCounterpart();
- MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
+ final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI;
+ MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), groupChat);
this.sendMessagePacket(conversation.getAccount(), packet);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index eec30ff..b6588a7 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -16,8 +16,6 @@ import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.text.Editable;
import android.text.InputType;
-import android.text.TextWatcher;
-import android.text.style.StyleSpan;
import android.util.Log;
import android.util.Pair;
import android.view.ContextMenu;
@@ -49,7 +47,9 @@ import net.java.otr4j.session.SessionStatus;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -63,6 +63,7 @@ import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection;
@@ -75,7 +76,6 @@ import eu.siacs.conversations.ui.adapter.MessageAdapter;
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
import eu.siacs.conversations.ui.widget.EditMessage;
-import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.NickValidityChecker;
import eu.siacs.conversations.utils.StylingHelper;
@@ -1394,12 +1394,51 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
}
} else {
+ final MucOptions mucOptions = conversation.getMucOptions();
+ final List<MucOptions.User> allUsers = mucOptions.getUsers();
+ final Set<ReadByMarker> addedMarkers = new HashSet<>();
ChatState state = ChatState.COMPOSING;
List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state,5);
if (users.size() == 0) {
state = ChatState.PAUSED;
users = conversation.getMucOptions().getUsersWithChatState(state, 5);
-
+ }
+ int markersAdded = 0;
+ if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
+ //addedMarkers.addAll(ReadByMarker.from(users));
+ for (int i = this.messageList.size() - 1; i >= 0; --i) {
+ final Set<ReadByMarker> markersForMessage = messageList.get(i).getReadByMarkers();
+ final List<MucOptions.User> shownMarkers = new ArrayList<>();
+ for (ReadByMarker marker : markersForMessage) {
+ if (!ReadByMarker.contains(marker, addedMarkers)) {
+ addedMarkers.add(marker); //may be put outside this condition. set should do dedup anyway
+ MucOptions.User user = mucOptions.findUser(marker);
+ if (user != null && !users.contains(user)) {
+ shownMarkers.add(user);
+ }
+ }
+ }
+ final ReadByMarker markerForSender = ReadByMarker.from(messageList.get(i));
+ final Message statusMessage;
+ if (shownMarkers.size() > 1) {
+ statusMessage = Message.createStatusMessage(conversation, getString(R.string.contacts_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers)));
+ statusMessage.setCounterparts(shownMarkers);
+ } else if (shownMarkers.size() == 1) {
+ statusMessage = Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, UIHelper.getDisplayName(shownMarkers.get(0))));
+ statusMessage.setCounterpart(shownMarkers.get(0).getFullJid());
+ statusMessage.setTrueCounterpart(shownMarkers.get(0).getRealJid());
+ } else {
+ statusMessage = null;
+ }
+ if (statusMessage != null) {
+ ++markersAdded;
+ this.messageList.add(i + 1, statusMessage);
+ }
+ addedMarkers.add(markerForSender);
+ if (ReadByMarker.allUsersRepresented(allUsers, addedMarkers)) {
+ break;
+ }
+ }
}
if (users.size() > 0) {
Message statusMessage;
@@ -1410,15 +1449,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
statusMessage.setTrueCounterpart(user.getRealJid());
statusMessage.setCounterpart(user.getFullJid());
} else {
- StringBuilder builder = new StringBuilder();
- for(MucOptions.User user : users) {
- if (builder.length() != 0) {
- builder.append(", ");
- }
- builder.append(UIHelper.getDisplayName(user));
- }
int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing;
- statusMessage = Message.createStatusMessage(conversation, getString(id, builder.toString()));
+ statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.concatNames(users)));
+ statusMessage.setCounterparts(users);
}
this.messageList.add(statusMessage);
}
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 8a115a1..3530ca5 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -6,11 +6,13 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
+import android.support.annotation.ColorInt;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableString;
@@ -709,7 +711,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (conversation.getMode() == Conversation.MODE_SINGLE) {
showAvatar = true;
loadAvatar(message,viewHolder.contact_picture,activity.getPixel(32));
- } else if (message.getCounterpart() != null ){
+ } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) {
showAvatar = true;
loadAvatar(message,viewHolder.contact_picture,activity.getPixel(32));
} else {
@@ -1052,9 +1054,15 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (bm != null) {
cancelPotentialWork(message, imageView);
imageView.setImageBitmap(bm);
- imageView.setBackgroundColor(0x00000000);
+ imageView.setBackgroundColor(Color.TRANSPARENT);
} else {
- imageView.setBackgroundColor(UIHelper.getColorForName(UIHelper.getMessageDisplayName(message)));
+ @ColorInt int bg;
+ if (message.getType() == Message.TYPE_STATUS && message.getCounterparts() != null && message.getCounterparts().size() > 1) {
+ bg = Color.TRANSPARENT;
+ } else {
+ bg = UIHelper.getColorForName(UIHelper.getMessageDisplayName(message));
+ }
+ imageView.setBackgroundColor(bg);
imageView.setImageDrawable(null);
final BitmapWorkerTask task = new BitmapWorkerTask(imageView, size);
final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index 3c2ad13..c678d44 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -8,6 +8,9 @@ import android.widget.PopupMenu;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
@@ -28,6 +31,35 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class UIHelper {
+
+ private static int COLORS[] = {
+ 0xFFE91E63, //pink 500
+ 0xFFAD1457, //pink 800
+ 0xFF9C27B0, //purple 500
+ 0xFF6A1B9A, //purple 800
+ 0xFF673AB7, //deep purple 500,
+ 0xFF4527A0, //deep purple 800,
+ 0xFF3F51B5, //indigo 500,
+ 0xFF283593, //indigo 800
+ 0xFF2196F3, //blue 500
+ 0xFF1565C0, //blue 800
+ 0xFF03A9F4, //light blue 500
+ 0xFF0277BD, //light blue 800
+ 0xFF00BCD4, //cyan 500
+ 0xFF00838F, //cyan 800
+ 0xFF009688, //teal 500,
+ 0xFF00695C, //teal 800,
+ //0xFF558B2F, //light green 800
+ 0xFFC0CA33, //lime 600
+ 0xFF9E9D24, //lime 800
+ 0xFFEF6C00, //orange 800
+ 0xFFD84315, //deep orange 800,
+ 0xFF795548, //brown 500,
+ //0xFF4E342E, //brown 800
+ 0xFF607D8B, //blue grey 500,
+ 0xFF37474F //blue grey 800
+ };
+
private static final List<String> LOCATION_QUESTIONS = Arrays.asList(
"where are you", //en
"where are you now", //en
@@ -150,10 +182,18 @@ public class UIHelper {
if (name == null || name.isEmpty()) {
return 0xFF202020;
}
- int colors[] = {0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
- 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
- 0xFF795548, 0xFF607d8b};
- return colors[(int) ((name.hashCode() & 0xffffffffl) % colors.length)];
+ return COLORS[getIntForName(name) % COLORS.length];
+ }
+
+ private static int getIntForName(String name) {
+ try {
+ final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+ messageDigest.update(name.getBytes());
+ byte[] bytes = messageDigest.digest();
+ return Math.abs(new BigInteger(bytes).intValue());
+ } catch (Exception e) {
+ return 0;
+ }
}
public static Pair<String,Boolean> getMessagePreview(final Context context, final Message message) {
@@ -312,8 +352,29 @@ public class UIHelper {
if (contact != null) {
return contact.getDisplayName();
} else {
- return user.getName();
+ final String name = user.getName();
+ if (name != null) {
+ return name;
+ }
+ final Jid realJid = user.getRealJid();
+ if (realJid != null) {
+ return JidHelper.localPartOrFallback(realJid);
+ }
+ return null;
+ }
+ }
+
+ public static String concatNames(List<MucOptions.User> users) {
+ StringBuilder builder = new StringBuilder();
+ final boolean shortNames = users.size() >= 3;
+ for(MucOptions.User user : users) {
+ if (builder.length() != 0) {
+ builder.append(", ");
+ }
+ final String name = UIHelper.getDisplayName(user);
+ builder.append(shortNames ? name.split("\\s+")[0] : name);
}
+ return builder.toString();
}
public static String getFileDescriptionString(final Context context, final Message message) {
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 932d5e4..e541797 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -247,6 +247,7 @@
<string name="contact_added_you">Contact added you to contact list</string>
<string name="add_back">Add back</string>
<string name="contact_has_read_up_to_this_point">%s has read up to this point</string>
+ <string name="contacts_have_read_up_to_this_point">%s have read up to this point</string>
<string name="publish">Publish</string>
<string name="touch_to_choose_picture">Touch avatar to select picture from gallery</string>
<string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string>