aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/de/pixart/messenger/entities/Conversation.java11
-rw-r--r--src/main/java/de/pixart/messenger/entities/Message.java45
-rw-r--r--src/main/java/de/pixart/messenger/entities/MucOptions.java34
-rw-r--r--src/main/java/de/pixart/messenger/entities/ReadByMarker.java166
-rw-r--r--src/main/java/de/pixart/messenger/generator/MessageGenerator.java11
-rw-r--r--src/main/java/de/pixart/messenger/parser/MessageParser.java26
-rw-r--r--src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java7
-rw-r--r--src/main/java/de/pixart/messenger/services/AvatarService.java84
-rw-r--r--src/main/java/de/pixart/messenger/services/XmppConnectionService.java5
-rw-r--r--src/main/java/de/pixart/messenger/ui/ConversationFragment.java51
-rw-r--r--src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java103
-rw-r--r--src/main/java/de/pixart/messenger/utils/UIHelper.java69
-rw-r--r--src/main/res/layout/message_status.xml31
-rw-r--r--src/main/res/values-v21/themes.xml3
-rw-r--r--src/main/res/values/attrs.xml3
-rw-r--r--src/main/res/values/strings.xml2
-rw-r--r--src/main/res/values/themes.xml3
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>