basic support for XEP-0308: Last Message Correction. fixes #864

This commit is contained in:
Daniel Gultsch 2016-02-15 23:15:04 +01:00
parent 335058b78b
commit c0b3a3ff0c
42 changed files with 228 additions and 41 deletions

View file

@ -82,6 +82,7 @@ public class Conversation extends AbstractEntity implements Blockable {
private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
private String mLastReceivedOtrMessageId = null;
private String mFirstMamReference = null;
private Message correctingMessage;
public boolean hasMessagesLeftOnServer() {
return messagesLeftOnServer;
@ -226,6 +227,17 @@ public class Conversation extends AbstractEntity implements Blockable {
return null;
}
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart) {
synchronized (this.messages) {
for(Message message : this.messages) {
if(id.equals(message.getRemoteMsgId()) && counterpart.equals(message.getCounterpart())) {
return message;
}
}
}
return null;
}
public Message findSentMessageWithUuid(String id) {
synchronized (this.messages) {
for (Message message : this.messages) {
@ -294,6 +306,14 @@ public class Conversation extends AbstractEntity implements Blockable {
return getLongAttribute("last_clear_history", 0);
}
public void setCorrectingMessage(Message correctingMessage) {
this.correctingMessage = correctingMessage;
}
public Message getCorrectingMessage() {
return this.correctingMessage;
}
public interface OnMessageFound {
void onMessageFound(final Message message);
}

View file

@ -52,6 +52,7 @@ public class Message extends AbstractEntity {
public static final String STATUS = "status";
public static final String TYPE = "type";
public static final String CARBON = "carbon";
public static final String EDITED = "edited";
public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
@ -71,6 +72,7 @@ public class Message extends AbstractEntity {
protected int status;
protected int type;
protected boolean carbon = false;
protected String edited = null;
protected String relativeFilePath;
protected boolean read = true;
protected String remoteMsgId = null;
@ -104,7 +106,8 @@ public class Message extends AbstractEntity {
null,
null,
null,
true);
true,
null);
this.conversation = conversation;
}
@ -112,7 +115,8 @@ public class Message extends AbstractEntity {
final Jid trueCounterpart, final String body, final long timeSent,
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 serverMsgId, final String fingerprint, final boolean read,
final String edited) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
@ -128,6 +132,7 @@ public class Message extends AbstractEntity {
this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint;
this.read = read;
this.edited = edited;
}
public static Message fromCursor(Cursor cursor) {
@ -162,12 +167,13 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getInt(cursor.getColumnIndex(CARBON))>0,
cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
cursor.getInt(cursor.getColumnIndex(READ)) > 0);
cursor.getInt(cursor.getColumnIndex(READ)) > 0,
cursor.getString(cursor.getColumnIndex(EDITED)));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@ -211,7 +217,8 @@ public class Message extends AbstractEntity {
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint);
values.put(READ,read);
values.put(READ,read ? 1 : 0);
values.put(EDITED, edited);
return values;
}
@ -340,10 +347,22 @@ public class Message extends AbstractEntity {
this.carbon = carbon;
}
public void setEdited(String edited) {
this.edited = edited;
}
public boolean edited() {
return this.edited != null;
}
public void setTrueCounterpart(Jid trueCounterpart) {
this.trueCounterpart = trueCounterpart;
}
public Jid getTrueCounterpart() {
return this.trueCounterpart;
}
public Transferable getTransferable() {
return this.transferable;
}
@ -421,6 +440,7 @@ public class Message extends AbstractEntity {
this.getEncryption() == message.getEncryption() &&
this.getCounterpart() != null &&
this.getCounterpart().equals(message.getCounterpart()) &&
this.edited() == message.edited() &&
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
!GeoHelper.isGeoUri(message.getBody()) &&
!GeoHelper.isGeoUri(this.body) &&
@ -510,6 +530,14 @@ public class Message extends AbstractEntity {
}
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getEditedId() {
return edited;
}
public enum Decision {
MUST,
SHOULD,

View file

@ -31,6 +31,7 @@ public abstract class AbstractGenerator {
"urn:xmpp:avatar:metadata+notify",
"http://jabber.org/protocol/nick+notify",
"urn:xmpp:ping",
"urn:xmpp:message-correct:0",
"jabber:iq:version",
"http://jabber.org/protocol/chatstates",
AxolotlService.PEP_DEVICE_LIST+"+notify"};

View file

@ -47,6 +47,9 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setFrom(account.getJid());
packet.setId(message.getUuid());
if (message.edited()) {
packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId());
}
return packet;
}

View file

@ -297,6 +297,8 @@ public class MessageParser extends AbstractParser implements
final String body = packet.getBody();
final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element replaceElement = packet.findChild("replace","urn:xmpp:message-correct:0");
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
@ -390,6 +392,33 @@ public class MessageParser extends AbstractParser implements
} else {
updateLastseen(timestamp, account, packet.getFrom(), true);
}
if (replacementId != null) {
Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, counterpart);
if (replacedMessage != null) {
final boolean fingerprintsMatch = replacedMessage.getAxolotlFingerprint() == null
|| replacedMessage.getAxolotlFingerprint().equals(message.getAxolotlFingerprint());
final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
if (fingerprintsMatch && (trueCountersMatch || conversation.getMode() == Conversation.MODE_SINGLE)) {
Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
replacedMessage.setBody(message.getBody());
replacedMessage.setEdited(replacedMessage.getRemoteMsgId());
replacedMessage.setRemoteMsgId(remoteMsgId);
if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
replacedMessage.markUnread();
}
mXmppConnectionService.updateMessage(replacedMessage);
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
sendMessageReceipts(account, packet);
}
return;
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received message correction but verification didn't check out");
}
}
}
boolean checkForDuplicates = query != null
|| (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay"))
|| message.getType() == Message.TYPE_PRIVATE;
@ -420,20 +449,7 @@ public class MessageParser extends AbstractParser implements
}
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
ArrayList<String> receiptsNamespaces = new ArrayList<>();
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
receiptsNamespaces.add("urn:xmpp:chat-markers:0");
}
if (packet.hasChild("request", "urn:xmpp:receipts")) {
receiptsNamespaces.add("urn:xmpp:receipts");
}
if (receiptsNamespaces.size() > 0) {
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
packet,
receiptsNamespaces,
packet.getType());
mXmppConnectionService.sendMessagePacket(account, receipt);
}
sendMessageReceipts(account, packet);
}
if (message.getStatus() == Message.STATUS_RECEIVED
@ -524,4 +540,21 @@ public class MessageParser extends AbstractParser implements
contact.setPresenceName(nick);
}
}
private void sendMessageReceipts(Account account, MessagePacket packet) {
ArrayList<String> receiptsNamespaces = new ArrayList<>();
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
receiptsNamespaces.add("urn:xmpp:chat-markers:0");
}
if (packet.hasChild("request", "urn:xmpp:receipts")) {
receiptsNamespaces.add("urn:xmpp:receipts");
}
if (receiptsNamespaces.size() > 0) {
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
packet,
receiptsNamespaces,
packet.getType());
mXmppConnectionService.sendMessagePacket(account, receipt);
}
}
}

View file

@ -51,7 +51,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 23;
private static final int DATABASE_VERSION = 24;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -161,6 +161,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, "
+ Message.CARBON + " INTEGER, "
+ Message.EDITED + " TEXT, "
+ Message.READ + " NUMBER DEFAULT 1, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
@ -370,6 +371,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (oldVersion < 23 && newVersion >= 23) {
db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
}
if (oldVersion < 24 && newVersion >= 24) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
}
}
public static synchronized DatabaseBackend getInstance(Context context) {
@ -586,6 +591,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ "=?", args);
}
public void updateMessage(Message message, String uuid) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {uuid};
db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
+ "=?", args);
}
public void readRoster(Roster roster) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor;

View file

@ -841,8 +841,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
final Conversation conversation = message.getConversation();
account.deactivateGracePeriod();
MessagePacket packet = null;
final boolean addToConversation = conversation.getMode() != Conversation.MODE_MULTI
|| account.getServerIdentity() != XmppConnection.Identity.SLACK;
final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI
|| account.getServerIdentity() != XmppConnection.Identity.SLACK)
&& !message.edited();
boolean saveInDb = addToConversation;
message.setStatus(Message.STATUS_WAITING);
@ -966,8 +967,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (addToConversation) {
conversation.add(message);
}
if (saveInDb && (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages())) {
databaseBackend.createMessage(message);
if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) {
if (saveInDb) {
databaseBackend.createMessage(message);
} else if (message.edited()) {
databaseBackend.updateMessage(message, message.getEditedId());
}
}
updateConversationUi();
}

View file

@ -8,7 +8,6 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.Nullable;
@ -40,6 +39,7 @@ import net.java.otr4j.session.SessionStatus;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@ -51,7 +51,6 @@ import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.services.XmppConnectionService;
@ -294,8 +293,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE);
break;
case CANCEL:
if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
conversation.setNextCounterpart(null);
if (conversation != null) {
if (conversation.getCorrectingMessage() != null) {
conversation.setCorrectingMessage(null);
mEditMessage.getEditableText().clear();
}
if (conversation.getMode() == Conversation.MODE_MULTI) {
conversation.setNextCounterpart(null);
}
updateChatMsgHint();
updateSendButton();
}
@ -330,12 +335,21 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (body.length() == 0 || this.conversation == null) {
return;
}
Message message = new Message(conversation, body, conversation.getNextEncryption());
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (conversation.getNextCounterpart() != null) {
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_PRIVATE);
final Message message;
if (conversation.getCorrectingMessage() == null) {
message = new Message(conversation, body, conversation.getNextEncryption());
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (conversation.getNextCounterpart() != null) {
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_PRIVATE);
}
}
} else {
message = conversation.getCorrectingMessage();
message.setBody(body);
message.setEdited(message.getUuid());
message.setUuid(UUID.randomUUID().toString());
conversation.setCorrectingMessage(null);
}
switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_OTR:
@ -356,7 +370,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
public void updateChatMsgHint() {
final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
if (multi && conversation.getNextCounterpart() != null) {
if (conversation.getCorrectingMessage() != null) {
this.mEditMessage.setHint(R.string.send_corrected_message);
} else if (multi && conversation.getNextCounterpart() != null) {
this.mEditMessage.setHint(getString(
R.string.send_private_message_to,
conversation.getNextCounterpart().getResourcepart()));
@ -487,8 +503,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
synchronized (this.messageList) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
@ -503,6 +518,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
activity.getMenuInflater().inflate(R.menu.message_context, menu);
menu.setHeaderTitle(R.string.message_options);
MenuItem copyText = menu.findItem(R.id.copy_text);
MenuItem correctMessage = menu.findItem(R.id.correct_message);
MenuItem shareWith = menu.findItem(R.id.share_with);
MenuItem sendAgain = menu.findItem(R.id.send_again);
MenuItem copyUrl = menu.findItem(R.id.copy_url);
@ -514,6 +530,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
&& m.treatAsDownloadable() != Message.Decision.MUST) {
copyText.setVisible(true);
}
if (m.getType() == Message.TYPE_TEXT
&& m.getStatus() != Message.STATUS_RECEIVED
&& !m.isCarbon()) {
correctMessage.setVisible(true);
}
if ((m.getType() != Message.TYPE_TEXT
&& m.getType() != Message.TYPE_PRIVATE
&& m.getTransferable() == null)
@ -550,6 +571,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
case R.id.copy_text:
copyText(selectedMessage);
return true;
case R.id.correct_message:
correctMessage(selectedMessage);
return true;
case R.id.send_again:
resendMessage(selectedMessage);
return true;
@ -652,6 +676,16 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
updateSendButton();
}
private void correctMessage(Message message) {
while(message.mergeable(message.next())) {
message = message.next();
}
this.conversation.setCorrectingMessage(message);
this.mEditMessage.getEditableText().clear();
this.mEditMessage.getEditableText().append(message.getBody());
}
protected void highlightInConference(String nick) {
String oldString = mEditMessage.getText().toString().trim();
if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
@ -958,9 +992,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
final Conversation c = this.conversation;
final SendButtonAction action;
final Presence.Status status;
final boolean empty = this.mEditMessage == null || this.mEditMessage.getText().length() == 0;
final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString();
final boolean empty = text.length() == 0;
final boolean conference = c.getMode() == Conversation.MODE_MULTI;
if (conference && !c.getAccount().httpUploadAvailable()) {
if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) {
action = SendButtonAction.CANCEL;
} else if (conference && !c.getAccount().httpUploadAvailable()) {
if (empty && c.getNextCounterpart() != null) {
action = SendButtonAction.CANCEL;
} else {
@ -1238,6 +1275,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
updateSendButton();
}
@Override
public void onTextChanged() {
if (conversation != null && conversation.getCorrectingMessage() != null) {
updateSendButton();
}
}
private int completionIndex = 0;
private int lastCompletionLength = 0;
private String incomplete;

View file

@ -69,6 +69,7 @@ public class EditMessage extends EditText {
this.isUserTyping = false;
this.keyboardListener.onTextDeleted();
}
this.keyboardListener.onTextChanged();
}
}
@ -84,6 +85,7 @@ public class EditMessage extends EditText {
void onTypingStarted();
void onTypingStopped();
void onTextDeleted();
void onTextChanged();
boolean onTabPressed(boolean repeated);
}

View file

@ -123,6 +123,16 @@ public class MessageAdapter extends ArrayAdapter<Message> {
if (viewHolder.indicatorReceived != null) {
viewHolder.indicatorReceived.setVisibility(View.GONE);
}
if (viewHolder.edit_indicator != null) {
if (message.edited()) {
viewHolder.edit_indicator.setVisibility(View.VISIBLE);
viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp);
viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f);
} else {
viewHolder.edit_indicator.setVisibility(View.GONE);
}
}
boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) {
@ -179,7 +189,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
if (message.getEncryption() == Message.ENCRYPTION_NONE) {
viewHolder.indicator.setVisibility(View.GONE);
} else {
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_secure_indicator_white : R.drawable.ic_secure_indicator);
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
XmppAxolotlSession.Trust trust = message.getConversation()
@ -463,6 +473,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
.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 = (TextView) view
@ -483,6 +494,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
.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 = (TextView) view
@ -701,6 +713,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected TextView status_message;
protected TextView encryption;
public Button load_more_messages;
public ImageView edit_indicator;
}
class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

View file

@ -91,7 +91,17 @@
android:layout_marginRight="4sp"
android:alpha="0.70"
android:gravity="center_vertical"
android:src="@drawable/ic_secure_indicator_white" />
android:src="@drawable/ic_lock_white_18dp" />
<ImageView
android:id="@+id/edit_indicator"
android:layout_width="?attr/TextSizeInfo"
android:layout_height="?attr/TextSizeInfo"
android:layout_gravity="center_vertical"
android:layout_marginRight="4sp"
android:alpha="0.70"
android:gravity="center_vertical"
android:src="@drawable/ic_mode_edit_white_18dp" />
<TextView
android:id="@+id/message_time"

View file

@ -91,7 +91,17 @@
android:layout_marginLeft="4sp"
android:alpha="0.54"
android:gravity="center_vertical"
android:src="@drawable/ic_secure_indicator" />
android:src="@drawable/ic_lock_black_18dp" />
<ImageView
android:id="@+id/edit_indicator"
android:layout_width="?attr/TextSizeInfo"
android:layout_height="?attr/TextSizeInfo"
android:layout_gravity="center_vertical"
android:layout_marginLeft="4sp"
android:alpha="0.54"
android:gravity="center_vertical"
android:src="@drawable/ic_mode_edit_black_18dp" />
<ImageView
android:id="@+id/indicator_received"

View file

@ -5,6 +5,10 @@
android:id="@+id/copy_text"
android:title="@string/copy_text"
android:visible="false"/>
<item
android:id="@+id/correct_message"
android:title="@string/correct_message"
android:visible="false"/>
<item
android:id="@+id/share_with"
android:title="@string/share_with"

View file

@ -593,4 +593,6 @@
<string name="selection_too_large">The selected area is too large</string>
<string name="no_accounts">(No activated accounts)</string>
<string name="this_field_is_required">This field is required</string>
<string name="correct_message">Correct message</string>
<string name="send_corrected_message">Send corrected message</string>
</resources>