From 28e005f926294bbb3f8c7621fb1484fa06b1ce9a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Nov 2017 01:53:04 +0100 Subject: [PATCH] send and show read markers in private, non-anonymous groups --- .../java/eu/siacs/conversations/Config.java | 2 +- .../conversations/entities/Conversation.java | 11 ++ .../siacs/conversations/entities/Message.java | 49 +++++- .../conversations/entities/MucOptions.java | 34 ++-- .../conversations/entities/ReadByMarker.java | 166 ++++++++++++++++++ .../generator/MessageGenerator.java | 11 +- .../conversations/parser/MessageParser.java | 21 ++- .../persistance/DatabaseBackend.java | 7 +- .../conversations/services/AvatarService.java | 84 ++++++++- .../services/XmppConnectionService.java | 6 +- .../ui/ConversationFragment.java | 57 ++++-- .../ui/adapter/MessageAdapter.java | 14 +- .../siacs/conversations/utils/UIHelper.java | 71 +++++++- src/main/res/values/strings.xml | 1 + 14 files changed, 481 insertions(+), 53 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/entities/ReadByMarker.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 7dc8c9f04..511af46f6 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 28b51b9af..44aea6928 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 d01ec2dcc..dcddb3eab 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 readByMarkers = new HashSet<>(); private Boolean isGeoUri = null; private Boolean isEmojisOnly = null; private Boolean treatAsDownloadable = null; private FileParams fileParams = null; + private List 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 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 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 counterparts) { + this.counterparts = counterparts; + } + + public List 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 7c66878b6..8414fd999 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 000000000..6767212dc --- /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 fromJson(JSONArray jsonArray) { + HashSet 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 from(Collection users) { + final HashSet 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 fromJsonString(String json) { + try { + return fromJson(new JSONArray(json)); + } catch (JSONException | NullPointerException e) { + return new HashSet<>(); + } + } + + public static JSONArray toJson(Set readByMarkers) { + JSONArray jsonArray = new JSONArray(); + for(ReadByMarker marker : readByMarkers) { + jsonArray.put(marker.toJson()); + } + return jsonArray; + } + + public static boolean contains(ReadByMarker needle, Set 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 users, Set 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 ec91cacc7..49355b1b1 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 d501bb964..8deae0380 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 34d62608d..de8dbb0a5 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 c7f97cd1f..cc517a83d 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 sizes = new ArrayList<>(); + final private HashMap> 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 keys = this.conversationDependentKeys.get(conversation.getUuid()); + if (keys == null) { + return; + } + LruCache 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 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 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 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 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 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 b9fa5744a..74472faec 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 eec30ff4b..b6588a75a 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 allUsers = mucOptions.getUsers(); + final Set addedMarkers = new HashSet<>(); ChatState state = ChatState.COMPOSING; List 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 markersForMessage = messageList.get(i).getReadByMarkers(); + final List 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 8a115a1b1..3530ca59d 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 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 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 3c2ad136a..c678d44f3 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 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 getMessagePreview(final Context context, final Message message) { @@ -312,10 +352,31 @@ 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 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) { if (message.getType() == Message.TYPE_IMAGE) { return context.getString(R.string.image); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 932d5e4ba..e541797d7 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -247,6 +247,7 @@ Contact added you to contact list Add back %s has read up to this point + %s have read up to this point Publish Touch avatar to select picture from gallery Please note: Everyone subscribed to your presence updates will be allowed to see this picture.