diff options
17 files changed, 569 insertions, 85 deletions
diff --git a/src/main/java/de/pixart/messenger/entities/Conversation.java b/src/main/java/de/pixart/messenger/entities/Conversation.java index 2d4ab926d..e5a2ec171 100644 --- a/src/main/java/de/pixart/messenger/entities/Conversation.java +++ b/src/main/java/de/pixart/messenger/entities/Conversation.java @@ -303,6 +303,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/de/pixart/messenger/entities/Message.java b/src/main/java/de/pixart/messenger/entities/Message.java index c51fa1638..2fa9d7c3a 100644 --- a/src/main/java/de/pixart/messenger/entities/Message.java +++ b/src/main/java/de/pixart/messenger/entities/Message.java @@ -9,6 +9,10 @@ import com.vdurmont.emoji.EmojiManager; 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 de.pixart.messenger.Config; import de.pixart.messenger.crypto.axolotl.FingerprintStatus; @@ -65,6 +69,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 "; @@ -91,12 +96,14 @@ 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 isXmppUri = 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; @@ -124,6 +131,7 @@ public class Message extends AbstractEntity { true, null, false, + null, null); } @@ -132,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; @@ -152,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) { @@ -197,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) { @@ -252,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; } @@ -420,6 +431,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()); @@ -522,7 +552,8 @@ public class Message extends AbstractEntity { !this.isXmppUri() && !message.isXmppUri() && ((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()) ); } @@ -536,6 +567,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 { } diff --git a/src/main/java/de/pixart/messenger/entities/MucOptions.java b/src/main/java/de/pixart/messenger/entities/MucOptions.java index f60c2643c..50ea4c2fb 100644 --- a/src/main/java/de/pixart/messenger/entities/MucOptions.java +++ b/src/main/java/de/pixart/messenger/entities/MucOptions.java @@ -11,6 +11,7 @@ import de.pixart.messenger.Config; import de.pixart.messenger.R; import de.pixart.messenger.utils.JidHelper; import de.pixart.messenger.utils.Namespace; +import de.pixart.messenger.utils.UIHelper; import de.pixart.messenger.xmpp.chatstate.ChatState; import de.pixart.messenger.xmpp.forms.Data; import de.pixart.messenger.xmpp.forms.Field; @@ -279,6 +280,10 @@ public class MucOptions { return options.getAccount(); } + public Conversation getConversation() { + return options.getConversation(); + } + public Jid getFullJid() { return fullJid; } @@ -518,6 +523,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; } @@ -661,17 +681,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/de/pixart/messenger/entities/ReadByMarker.java b/src/main/java/de/pixart/messenger/entities/ReadByMarker.java new file mode 100644 index 000000000..1536a7ccd --- /dev/null +++ b/src/main/java/de/pixart/messenger/entities/ReadByMarker.java @@ -0,0 +1,166 @@ +package de.pixart.messenger.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 de.pixart.messenger.xmpp.jid.InvalidJidException; +import de.pixart.messenger.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/de/pixart/messenger/generator/MessageGenerator.java b/src/main/java/de/pixart/messenger/generator/MessageGenerator.java index 5ed2266ef..4dae14757 100644 --- a/src/main/java/de/pixart/messenger/generator/MessageGenerator.java +++ b/src/main/java/de/pixart/messenger/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()); @@ -167,10 +170,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/de/pixart/messenger/parser/MessageParser.java b/src/main/java/de/pixart/messenger/parser/MessageParser.java index 54a4571cc..d5003dc11 100644 --- a/src/main/java/de/pixart/messenger/parser/MessageParser.java +++ b/src/main/java/de/pixart/messenger/parser/MessageParser.java @@ -29,6 +29,7 @@ import de.pixart.messenger.entities.Conversation; import de.pixart.messenger.entities.Message; import de.pixart.messenger.entities.MucOptions; import de.pixart.messenger.entities.Presence; +import de.pixart.messenger.entities.ReadByMarker; import de.pixart.messenger.entities.ServiceDiscoveryResult; import de.pixart.messenger.http.HttpConnectionManager; import de.pixart.messenger.services.MessageArchiveService; @@ -686,13 +687,36 @@ 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()); 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) { + if (conversation.getMucOptions().isSelf(counterpart)) { + if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections + mXmppConnectionService.markRead(conversation); + } + } else { + final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); + Jid trueJid = getTrueCounterpart(query != null ? mucUserElement : null, fallback); + ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); + if (message.addReadByMarker(readByMarker)) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'"); + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED); + 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/de/pixart/messenger/persistance/DatabaseBackend.java b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java index e88db114a..cef543018 100644 --- a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java +++ b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java @@ -58,7 +58,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; public static final String DATABASE_NAME = "history"; - public static final int DATABASE_VERSION = 37; // = Conversations DATABASE_VERSION + 1 + public static final int DATABASE_VERSION = 38; // = Conversations DATABASE_VERSION + 1 private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -193,6 +193,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 @@ -465,6 +466,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { + "=?", new String[]{account.getUuid()}); } } + + if (oldVersion < 38 && newVersion >= 38) { + 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/de/pixart/messenger/services/AvatarService.java b/src/main/java/de/pixart/messenger/services/AvatarService.java index 3d3af8d26..dc027d260 100644 --- a/src/main/java/de/pixart/messenger/services/AvatarService.java +++ b/src/main/java/de/pixart/messenger/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 de.pixart.messenger.Config; import de.pixart.messenger.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; @@ -188,6 +193,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(); + } } } @@ -198,17 +214,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); @@ -230,7 +265,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; } @@ -252,6 +286,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); } @@ -272,7 +331,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); @@ -324,11 +385,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/de/pixart/messenger/services/XmppConnectionService.java b/src/main/java/de/pixart/messenger/services/XmppConnectionService.java index 2728cd8cd..75995edb9 100644 --- a/src/main/java/de/pixart/messenger/services/XmppConnectionService.java +++ b/src/main/java/de/pixart/messenger/services/XmppConnectionService.java @@ -3591,11 +3591,12 @@ public class XmppConnectionService extends Service { if (this.markRead(conversation)) { updateConversationUi(); } - if (confirmMessages() && markable != null && markable.trusted() && markable.getRemoteMsgId() != null) { + if (confirmMessages() && markable != null && markable.trusted() && 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/de/pixart/messenger/ui/ConversationFragment.java b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java index 7cfaa5b02..75d599e3f 100644 --- a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java +++ b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java @@ -48,8 +48,10 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -63,6 +65,7 @@ import de.pixart.messenger.entities.DownloadableFile; import de.pixart.messenger.entities.Message; import de.pixart.messenger.entities.MucOptions; import de.pixart.messenger.entities.Presence; +import de.pixart.messenger.entities.ReadByMarker; import de.pixart.messenger.entities.Transferable; import de.pixart.messenger.entities.TransferablePlaceholder; import de.pixart.messenger.http.HttpDownloadConnection; @@ -1368,6 +1371,54 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (showLoadMoreMessages(conversation)) { this.messageList.add(0, Message.createLoadMoreMessage(conversation)); } + if (conversation.getMode() == Conversation.MODE_MULTI) { + 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; + } + } + } + } } } diff --git a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java index f21216039..4653ee3de 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/pixart/messenger/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; @@ -754,60 +756,42 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie switch (type) { case DATE_SEPARATOR: view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); - viewHolder.status_message = (TextView) view.findViewById(R.id.status_message); + viewHolder.status_message = view.findViewById(R.id.status_message); break; case SENT: - view = activity.getLayoutInflater().inflate( - R.layout.message_sent, parent, false); - viewHolder.message_box = (LinearLayout) view - .findViewById(R.id.message_box); - viewHolder.contact_picture = (ImageView) view - .findViewById(R.id.message_photo); - viewHolder.audioPlayer = (RelativeLayout) view.findViewById(R.id.audio_player); - viewHolder.download_button = (Button) view - .findViewById(R.id.download_button); - viewHolder.resend_button = (Button) view - .findViewById(R.id.resend_button); - viewHolder.indicator = (ImageView) view - .findViewById(R.id.security_indicator); - viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator); - viewHolder.image = (ImageView) view - .findViewById(R.id.message_image); - viewHolder.messageBody = (CopyTextView) view - .findViewById(R.id.message_body); - viewHolder.time = (TextView) view - .findViewById(R.id.message_time); - viewHolder.indicatorReceived = (ImageView) view - .findViewById(R.id.indicator_received); - viewHolder.indicatorRead = (ImageView) view - .findViewById(R.id.indicator_read); + view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.resend_button = view.findViewById(R.id.resend_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.indicatorRead = view.findViewById(R.id.indicator_read); break; case RECEIVED: - view = activity.getLayoutInflater().inflate( - R.layout.message_received, parent, false); - viewHolder.message_box = (LinearLayout) view - .findViewById(R.id.message_box); - viewHolder.contact_picture = (ImageView) view - .findViewById(R.id.message_photo); - viewHolder.audioPlayer = (RelativeLayout) view.findViewById(R.id.audio_player); - viewHolder.download_button = (Button) view - .findViewById(R.id.download_button); - viewHolder.indicator = (ImageView) view - .findViewById(R.id.security_indicator); - viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator); - viewHolder.image = (ImageView) view - .findViewById(R.id.message_image); - viewHolder.messageBody = (CopyTextView) view - .findViewById(R.id.message_body); - viewHolder.time = (TextView) view - .findViewById(R.id.message_time); - viewHolder.indicatorReceived = (ImageView) view - .findViewById(R.id.indicator_received); - viewHolder.encryption = (TextView) view.findViewById(R.id.message_encryption); + view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.encryption = view.findViewById(R.id.message_encryption); break; case STATUS: view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); - viewHolder.load_more_messages = (Button) view.findViewById(R.id.load_more_messages); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.status_message = view.findViewById(R.id.status_message); + viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); break; default: throw new AssertionError("Unknown view type"); @@ -844,7 +828,22 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie } }); } else { + viewHolder.status_message.setVisibility(View.VISIBLE); viewHolder.load_more_messages.setVisibility(View.GONE); + viewHolder.status_message.setText(message.getBody()); + boolean showAvatar; + if (conversation.getMode() == Conversation.MODE_MULTI && (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0))) { + showAvatar = true; + loadAvatar(message, viewHolder.contact_picture, activity.getPixel(32)); + } else { + showAvatar = false; + } + if (showAvatar) { + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture.setVisibility(View.VISIBLE); + } else { + viewHolder.contact_picture.setVisibility(View.GONE); + } } return view; } else { @@ -1201,9 +1200,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/de/pixart/messenger/utils/UIHelper.java b/src/main/java/de/pixart/messenger/utils/UIHelper.java index 0049a2aa8..953ffcf81 100644 --- a/src/main/java/de/pixart/messenger/utils/UIHelper.java +++ b/src/main/java/de/pixart/messenger/utils/UIHelper.java @@ -8,6 +8,8 @@ import android.widget.PopupMenu; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.math.BigInteger; +import java.security.MessageDigest; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -28,6 +30,34 @@ import de.pixart.messenger.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 +180,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) { @@ -321,8 +359,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/layout/message_status.xml b/src/main/res/layout/message_status.xml index e2200844c..afdf317d7 100644 --- a/src/main/res/layout/message_status.xml +++ b/src/main/res/layout/message_status.xml @@ -2,6 +2,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" + xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:paddingBottom="5dp" android:paddingLeft="8dp" @@ -18,4 +19,34 @@ android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> + <com.makeramen.roundedimageview.RoundedImageView + android:id="@+id/message_photo" + android:visibility="gone" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_marginRight="-1.5dp" + android:padding="0dp" + android:scaleType="fitXY" + android:src="@drawable/ic_profile" + app:riv_border_width="1dp" + app:riv_border_color="@color/grey500" + app:riv_corner_radius="16dp"/> + + <TextView + android:id="@+id/status_message" + android:visibility="gone" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minHeight="32dp" + android:layout_centerVertical="true" + android:gravity="center_vertical" + android:layout_marginLeft="8dp" + android:layout_toEndOf="@+id/message_photo" + android:layout_toRightOf="@+id/message_photo" + android:text="@string/contact_has_read_up_to_this_point" + android:textColor="?attr/color_text_secondary" + android:textSize="?attr/TextSizeInfo" + android:textStyle="italic"/> </RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/values-v21/themes.xml b/src/main/res/values-v21/themes.xml index 86346775f..bbb2184f1 100644 --- a/src/main/res/values-v21/themes.xml +++ b/src/main/res/values-v21/themes.xml @@ -16,6 +16,9 @@ <item name="TextSizeHeadline">18sp</item> <item name="TextSeparation">5sp</item> + <item name="attr/color_text_primary">@color/black87</item> + <item name="attr/color_text_secondary">@color/black54</item> + <item name="attr/dialog_horizontal_padding">24dp</item> <item name="attr/dialog_vertical_padding">16dp</item> diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index a315ab796..3154a8443 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -7,6 +7,9 @@ <attr name="TextSeparation" format="dimension"/> <attr name="IconSize" format="dimension" /> + <attr name="color_text_primary" format="reference|color" /> + <attr name="color_text_secondary" format="reference|color" /> + <attr name="icon_add_group" format="reference" /> <attr name="icon_add_person" format="reference" /> <attr name="icon_cancel" format="reference" /> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f92be75bb..272f18a4e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -736,4 +736,6 @@ <string name="install_update">Install update?</string> <string name="highlight_in_muc">highlight user</string> <string name="no_application_found_to_open_link">No application found to open link</string> + <string name="contacts_have_read_up_to_this_point">%s have read up to this point</string> + <string name="contact_has_read_up_to_this_point">%s has read up to this point</string> </resources> diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 61ffc19ae..4dc939b2d 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -12,6 +12,9 @@ <item name="TextSizeHeadline">18sp</item> <item name="TextSeparation">5sp</item> + <item name="attr/color_text_primary">@color/black87</item> + <item name="attr/color_text_secondary">@color/black54</item> + <item name="attr/icon_add_group">@drawable/ic_group_add_white_24dp</item> <item name="attr/icon_add_person">@drawable/ic_person_add_white_24dp</item> <item name="attr/icon_cancel">@drawable/ic_cancel_white_24dp</item> |