send and show read markers in private, non-anonymous groups

This commit is contained in:
Daniel Gultsch 2017-11-19 01:53:04 +01:00
parent 1c65a17ff1
commit 28e005f926
14 changed files with 481 additions and 53 deletions

View file

@ -82,7 +82,7 @@ public final class Config {
public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY; public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY;
public static final boolean REMOVE_BROKEN_DEVICES = false; public static final boolean REMOVE_BROKEN_DEVICES = false;
public static final boolean OMEMO_PADDING = 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 public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb

View file

@ -289,6 +289,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null; 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) { public boolean hasMessageWithCounterpart(Jid counterpart) {
synchronized (this.messages) { synchronized (this.messages) {
for(Message message : this.messages) { for(Message message : this.messages) {

View file

@ -3,9 +3,17 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; 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.Config;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; 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 FINGERPRINT = "axolotl_fingerprint";
public static final String READ = "read"; public static final String READ = "read";
public static final String ERROR_MESSAGE = "errorMsg"; public static final String ERROR_MESSAGE = "errorMsg";
public static final String READ_BY_MARKERS = "readByMarkers";
public static final String ME_COMMAND = "/me "; public static final String ME_COMMAND = "/me ";
@ -88,11 +97,13 @@ public class Message extends AbstractEntity {
private Message mPreviousMessage = null; private Message mPreviousMessage = null;
private String axolotlFingerprint = null; private String axolotlFingerprint = null;
private String errorMessage = null; private String errorMessage = null;
protected Set<ReadByMarker> readByMarkers = new HashSet<>();
private Boolean isGeoUri = null; private Boolean isGeoUri = null;
private Boolean isEmojisOnly = null; private Boolean isEmojisOnly = null;
private Boolean treatAsDownloadable = null; private Boolean treatAsDownloadable = null;
private FileParams fileParams = null; private FileParams fileParams = null;
private List<MucOptions.User> counterparts;
private Message(Conversation conversation) { private Message(Conversation conversation) {
this.conversation = conversation; this.conversation = conversation;
@ -120,6 +131,7 @@ public class Message extends AbstractEntity {
true, true,
null, null,
false, false,
null,
null); null);
} }
@ -128,7 +140,7 @@ public class Message extends AbstractEntity {
final int encryption, final int status, final int type, final boolean carbon, final int encryption, final int status, final int type, final boolean carbon,
final String remoteMsgId, final String relativeFilePath, final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint, final boolean read, 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.conversation = conversation;
this.uuid = uuid; this.uuid = uuid;
this.conversationUuid = conversationUUid; this.conversationUuid = conversationUUid;
@ -148,6 +160,7 @@ public class Message extends AbstractEntity {
this.edited = edited; this.edited = edited;
this.oob = oob; this.oob = oob;
this.errorMessage = errorMessage; this.errorMessage = errorMessage;
this.readByMarkers = new HashSet<>();
} }
public static Message fromCursor(Cursor cursor, Conversation conversation) { public static Message fromCursor(Cursor cursor, Conversation conversation) {
@ -193,7 +206,8 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(READ)) > 0, cursor.getInt(cursor.getColumnIndex(READ)) > 0,
cursor.getString(cursor.getColumnIndex(EDITED)), cursor.getString(cursor.getColumnIndex(EDITED)),
cursor.getInt(cursor.getColumnIndex(OOB)) > 0, 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) { public static Message createStatusMessage(Conversation conversation, String body) {
@ -248,6 +262,7 @@ public class Message extends AbstractEntity {
values.put(EDITED, edited); values.put(EDITED, edited);
values.put(OOB, oob ? 1 : 0); values.put(OOB, oob ? 1 : 0);
values.put(ERROR_MESSAGE,errorMessage); values.put(ERROR_MESSAGE,errorMessage);
values.put(READ_BY_MARKERS,ReadByMarker.toJson(readByMarkers).toString());
return values; return values;
} }
@ -415,6 +430,25 @@ public class Message extends AbstractEntity {
this.transferable = transferable; 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) { public boolean similar(Message message) {
if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) { if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
return this.serverMsgId.equals(message.getServerMsgId()); return this.serverMsgId.equals(message.getServerMsgId());
@ -515,7 +549,8 @@ public class Message extends AbstractEntity {
!this.bodyIsOnlyEmojis() && !this.bodyIsOnlyEmojis() &&
!message.bodyIsOnlyEmojis() && !message.bodyIsOnlyEmojis() &&
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && ((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 static class MergeSeparator {}
public SpannableStringBuilder getMergedBody() { public SpannableStringBuilder getMergedBody() {

View file

@ -11,6 +11,7 @@ import java.util.Set;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.utils.JidHelper; import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Data;
@ -280,6 +281,10 @@ public class MucOptions {
return options.getAccount(); return options.getAccount();
} }
public Conversation getConversation() {
return options.getConversation();
}
public Jid getFullJid() { public Jid getFullJid() {
return fullJid; return fullJid;
} }
@ -521,6 +526,21 @@ public class MucOptions {
return null; 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) { public boolean isContactInRoom(Contact contact) {
return findUserByRealJid(contact.getJid().toBareJid()) != null; return findUserByRealJid(contact.getJid().toBareJid()) != null;
} }
@ -655,17 +675,9 @@ public class MucOptions {
if (builder.length() != 0) { if (builder.length() != 0) {
builder.append(", "); builder.append(", ");
} }
Contact contact = user.getContact(); String name = UIHelper.getDisplayName(user);
if (contact != null && !contact.getDisplayName().isEmpty()) { if (name != null) {
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]); builder.append(name.split("\\s+")[0]);
} else if (jid != null) {
builder.append(jid.getLocalpart());
}
} }
} }
return builder.toString(); return builder.toString();

View file

@ -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;
}
}

View file

@ -39,7 +39,6 @@ public class MessageGenerator extends AbstractGenerator {
if (conversation.getMode() == Conversation.MODE_SINGLE) { if (conversation.getMode() == Conversation.MODE_SINGLE) {
packet.setTo(message.getCounterpart()); packet.setTo(message.getCounterpart());
packet.setType(MessagePacket.TYPE_CHAT); packet.setType(MessagePacket.TYPE_CHAT);
packet.addChild("markable", "urn:xmpp:chat-markers:0");
if (this.mXmppConnectionService.indicateReceived()) { if (this.mXmppConnectionService.indicateReceived()) {
packet.addChild("request", "urn:xmpp:receipts"); packet.addChild("request", "urn:xmpp:receipts");
} }
@ -54,6 +53,10 @@ public class MessageGenerator extends AbstractGenerator {
packet.setTo(message.getCounterpart().toBareJid()); packet.setTo(message.getCounterpart().toBareJid());
packet.setType(MessagePacket.TYPE_GROUPCHAT); 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.setFrom(account.getJid());
packet.setId(message.getUuid()); packet.setId(message.getUuid());
packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id",message.getUuid()); packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id",message.getUuid());
@ -170,10 +173,10 @@ public class MessageGenerator extends AbstractGenerator {
return packet; 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(); MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT); packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
packet.setTo(to); packet.setTo(groupChat ? to.toBareJid() : to);
packet.setFrom(account.getJid()); packet.setFrom(account.getJid());
Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0"); Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0");
received.setAttribute("id", id); received.setAttribute("id", id);

View file

@ -29,6 +29,7 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.MessageArchiveService; 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"); Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
if (displayed != null) { if (displayed != null) {
final String id = displayed.getAttribute("id");
if (packet.fromAccount(account)) { 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())) { if (conversation != null && (query == null || query.isCatchup())) {
mXmppConnectionService.markRead(conversation); 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 { } 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(); Message message = displayedMessage == null ? null : displayedMessage.prev();
while (message != null while (message != null
&& message.getStatus() == Message.STATUS_SEND_RECEIVED && message.getStatus() == Message.STATUS_SEND_RECEIVED

View file

@ -60,7 +60,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null; private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history"; 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 " private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -197,6 +197,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.READ + " NUMBER DEFAULT 1, " + Message.READ + " NUMBER DEFAULT 1, "
+ Message.OOB + " INTEGER, " + Message.OOB + " INTEGER, "
+ Message.ERROR_MESSAGE + " TEXT," + Message.ERROR_MESSAGE + " TEXT,"
+ Message.READ_BY_MARKERS + " TEXT,"
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES " + Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID + Conversation.TABLENAME + "(" + Conversation.UUID
@ -454,6 +455,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ "=?", new String[]{account.getUuid()}); + "=?", 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) { private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {

View file

@ -10,10 +10,14 @@ import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.util.LruCache;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
@ -39,6 +43,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
private static final String PREFIX_GENERIC = "generic"; private static final String PREFIX_GENERIC = "generic";
final private ArrayList<Integer> sizes = new ArrayList<>(); final private ArrayList<Integer> sizes = new ArrayList<>();
final private HashMap<String,Set<String>> conversationDependentKeys = new HashMap<>();
protected XmppConnectionService mXmppConnectionService = null; protected XmppConnectionService mXmppConnectionService = null;
@ -184,6 +189,17 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
clear(conversation.getContact()); clear(conversation.getContact());
} else { } else {
clear(conversation.getMucOptions()); 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; return bitmap;
} }
final List<MucOptions.User> users = mucOptions.getUsers(5); 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(); 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); Canvas canvas = new Canvas(bitmap);
bitmap.eraseColor(TRANSPARENT); bitmap.eraseColor(TRANSPARENT);
if (count == 0) { if (count == 0) {
String name = mucOptions.getConversation().getName(); throw new AssertionError("Unable to draw tiles for 0 users");
drawTile(canvas, name, 0, 0, size, size);
} else if (count == 1) { } else if (count == 1) {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); 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) { } else if (count == 2) {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
drawTile(canvas, users.get(1), size / 2 + 1, 0, size, 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, drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1,
size, size); size, size);
} }
this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
return bitmap; return bitmap;
} }
@ -248,6 +282,31 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
+ "_" + String.valueOf(size); + "_" + 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) { public Bitmap get(Account account, int size) {
return get(account, size, false); return get(account, size, false);
} }
@ -268,7 +327,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
public Bitmap get(Message message, int size, boolean cachedOnly) { public Bitmap get(Message message, int size, boolean cachedOnly) {
final Conversation conversation = message.getConversation(); 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(); Contact c = message.getContact();
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) { if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
return get(c, size, cachedOnly); return get(c, size, cachedOnly);
@ -320,11 +381,16 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (bitmap != null || cachedOnly) { if (bitmap != null || cachedOnly) {
return bitmap; 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); Canvas canvas = new Canvas(bitmap);
final String trimmedName = name == null ? "" : name.trim(); final String trimmedName = name == null ? "" : name.trim();
drawTile(canvas, trimmedName, 0, 0, size, size); drawTile(canvas, trimmedName, 0, 0, size, size);
mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
return bitmap; return bitmap;
} }

View file

@ -3394,11 +3394,13 @@ public class XmppConnectionService extends Service {
if (confirmMessages() if (confirmMessages()
&& markable != null && markable != null
&& markable.trusted() && 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()); Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
Account account = conversation.getAccount(); Account account = conversation.getAccount();
final Jid to = markable.getCounterpart(); 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); this.sendMessagePacket(conversation.getAccount(), packet);
} }
} }

View file

@ -16,8 +16,6 @@ import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat; import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.text.Editable; import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.text.TextWatcher;
import android.text.style.StyleSpan;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.ContextMenu; import android.view.ContextMenu;
@ -49,7 +47,9 @@ import net.java.otr4j.session.SessionStatus;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; 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.Message;
import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection; 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.OnContactPictureClicked;
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
import eu.siacs.conversations.ui.widget.EditMessage; 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.MessageUtils;
import eu.siacs.conversations.utils.NickValidityChecker; import eu.siacs.conversations.utils.NickValidityChecker;
import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.StylingHelper;
@ -1394,12 +1394,51 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} }
} }
} else { } else {
final MucOptions mucOptions = conversation.getMucOptions();
final List<MucOptions.User> allUsers = mucOptions.getUsers();
final Set<ReadByMarker> addedMarkers = new HashSet<>();
ChatState state = ChatState.COMPOSING; ChatState state = ChatState.COMPOSING;
List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state,5); List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state,5);
if (users.size() == 0) { if (users.size() == 0) {
state = ChatState.PAUSED; state = ChatState.PAUSED;
users = conversation.getMucOptions().getUsersWithChatState(state, 5); 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) { if (users.size() > 0) {
Message statusMessage; Message statusMessage;
@ -1410,15 +1449,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
statusMessage.setTrueCounterpart(user.getRealJid()); statusMessage.setTrueCounterpart(user.getRealJid());
statusMessage.setCounterpart(user.getFullJid()); statusMessage.setCounterpart(user.getFullJid());
} else { } 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; 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); this.messageList.add(statusMessage);
} }

View file

@ -6,11 +6,13 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.ColorInt;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
@ -709,7 +711,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (conversation.getMode() == Conversation.MODE_SINGLE) { if (conversation.getMode() == Conversation.MODE_SINGLE) {
showAvatar = true; showAvatar = true;
loadAvatar(message,viewHolder.contact_picture,activity.getPixel(32)); 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; showAvatar = true;
loadAvatar(message,viewHolder.contact_picture,activity.getPixel(32)); loadAvatar(message,viewHolder.contact_picture,activity.getPixel(32));
} else { } else {
@ -1052,9 +1054,15 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (bm != null) { if (bm != null) {
cancelPotentialWork(message, imageView); cancelPotentialWork(message, imageView);
imageView.setImageBitmap(bm); imageView.setImageBitmap(bm);
imageView.setBackgroundColor(0x00000000); imageView.setBackgroundColor(Color.TRANSPARENT);
} else { } 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); imageView.setImageDrawable(null);
final BitmapWorkerTask task = new BitmapWorkerTask(imageView, size); final BitmapWorkerTask task = new BitmapWorkerTask(imageView, size);
final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);

View file

@ -8,6 +8,9 @@ import android.widget.PopupMenu;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
@ -28,6 +31,35 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class UIHelper { 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( private static final List<String> LOCATION_QUESTIONS = Arrays.asList(
"where are you", //en "where are you", //en
"where are you now", //en "where are you now", //en
@ -150,10 +182,18 @@ public class UIHelper {
if (name == null || name.isEmpty()) { if (name == null || name.isEmpty()) {
return 0xFF202020; return 0xFF202020;
} }
int colors[] = {0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5, return COLORS[getIntForName(name) % COLORS.length];
0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722, }
0xFF795548, 0xFF607d8b};
return colors[(int) ((name.hashCode() & 0xffffffffl) % 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) { public static Pair<String,Boolean> getMessagePreview(final Context context, final Message message) {
@ -312,8 +352,29 @@ public class UIHelper {
if (contact != null) { if (contact != null) {
return contact.getDisplayName(); return contact.getDisplayName();
} else { } 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) { public static String getFileDescriptionString(final Context context, final Message message) {

View file

@ -247,6 +247,7 @@
<string name="contact_added_you">Contact added you to contact list</string> <string name="contact_added_you">Contact added you to contact list</string>
<string name="add_back">Add back</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="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="publish">Publish</string>
<string name="touch_to_choose_picture">Touch avatar to select picture from gallery</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> <string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string>