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 ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
private String mLastReceivedOtrMessageId = null; private String mLastReceivedOtrMessageId = null;
private String mFirstMamReference = null; private String mFirstMamReference = null;
private Message correctingMessage;
public boolean hasMessagesLeftOnServer() { public boolean hasMessagesLeftOnServer() {
return messagesLeftOnServer; return messagesLeftOnServer;
@ -226,6 +227,17 @@ public class Conversation extends AbstractEntity implements Blockable {
return null; 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) { public Message findSentMessageWithUuid(String id) {
synchronized (this.messages) { synchronized (this.messages) {
for (Message message : this.messages) { for (Message message : this.messages) {
@ -294,6 +306,14 @@ public class Conversation extends AbstractEntity implements Blockable {
return getLongAttribute("last_clear_history", 0); return getLongAttribute("last_clear_history", 0);
} }
public void setCorrectingMessage(Message correctingMessage) {
this.correctingMessage = correctingMessage;
}
public Message getCorrectingMessage() {
return this.correctingMessage;
}
public interface OnMessageFound { public interface OnMessageFound {
void onMessageFound(final Message message); 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 STATUS = "status";
public static final String TYPE = "type"; public static final String TYPE = "type";
public static final String CARBON = "carbon"; public static final String CARBON = "carbon";
public static final String EDITED = "edited";
public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId"; public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath"; public static final String RELATIVE_FILE_PATH = "relativeFilePath";
@ -71,6 +72,7 @@ public class Message extends AbstractEntity {
protected int status; protected int status;
protected int type; protected int type;
protected boolean carbon = false; protected boolean carbon = false;
protected String edited = null;
protected String relativeFilePath; protected String relativeFilePath;
protected boolean read = true; protected boolean read = true;
protected String remoteMsgId = null; protected String remoteMsgId = null;
@ -104,7 +106,8 @@ public class Message extends AbstractEntity {
null, null,
null, null,
null, null,
true); true,
null);
this.conversation = conversation; this.conversation = conversation;
} }
@ -112,7 +115,8 @@ public class Message extends AbstractEntity {
final Jid trueCounterpart, final String body, final long timeSent, final Jid trueCounterpart, final String body, final long timeSent,
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) {
this.uuid = uuid; this.uuid = uuid;
this.conversationUuid = conversationUUid; this.conversationUuid = conversationUUid;
this.counterpart = counterpart; this.counterpart = counterpart;
@ -128,6 +132,7 @@ public class Message extends AbstractEntity {
this.serverMsgId = serverMsgId; this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint; this.axolotlFingerprint = fingerprint;
this.read = read; this.read = read;
this.edited = edited;
} }
public static Message fromCursor(Cursor cursor) { public static Message fromCursor(Cursor cursor) {
@ -162,12 +167,13 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)), 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(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)), 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) { 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(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId); values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint); values.put(FINGERPRINT, axolotlFingerprint);
values.put(READ,read); values.put(READ,read ? 1 : 0);
values.put(EDITED, edited);
return values; return values;
} }
@ -340,10 +347,22 @@ public class Message extends AbstractEntity {
this.carbon = carbon; this.carbon = carbon;
} }
public void setEdited(String edited) {
this.edited = edited;
}
public boolean edited() {
return this.edited != null;
}
public void setTrueCounterpart(Jid trueCounterpart) { public void setTrueCounterpart(Jid trueCounterpart) {
this.trueCounterpart = trueCounterpart; this.trueCounterpart = trueCounterpart;
} }
public Jid getTrueCounterpart() {
return this.trueCounterpart;
}
public Transferable getTransferable() { public Transferable getTransferable() {
return this.transferable; return this.transferable;
} }
@ -421,6 +440,7 @@ public class Message extends AbstractEntity {
this.getEncryption() == message.getEncryption() && this.getEncryption() == message.getEncryption() &&
this.getCounterpart() != null && this.getCounterpart() != null &&
this.getCounterpart().equals(message.getCounterpart()) && this.getCounterpart().equals(message.getCounterpart()) &&
this.edited() == message.edited() &&
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
!GeoHelper.isGeoUri(message.getBody()) && !GeoHelper.isGeoUri(message.getBody()) &&
!GeoHelper.isGeoUri(this.body) && !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 { public enum Decision {
MUST, MUST,
SHOULD, SHOULD,

View file

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

View file

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

View file

@ -297,6 +297,8 @@ public class MessageParser extends AbstractParser implements
final String body = packet.getBody(); final String body = packet.getBody();
final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); 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); final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status; int status;
final Jid counterpart; final Jid counterpart;
@ -390,6 +392,33 @@ public class MessageParser extends AbstractParser implements
} else { } else {
updateLastseen(timestamp, account, packet.getFrom(), true); 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 boolean checkForDuplicates = query != null
|| (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay")) || (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay"))
|| message.getType() == Message.TYPE_PRIVATE; || message.getType() == Message.TYPE_PRIVATE;
@ -420,20 +449,7 @@ public class MessageParser extends AbstractParser implements
} }
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
ArrayList<String> receiptsNamespaces = new ArrayList<>(); sendMessageReceipts(account, packet);
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);
}
} }
if (message.getStatus() == Message.STATUS_RECEIVED if (message.getStatus() == Message.STATUS_RECEIVED
@ -524,4 +540,21 @@ public class MessageParser extends AbstractParser implements
contact.setPresenceName(nick); 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 DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history"; 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 " private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -161,6 +161,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.SERVER_MSG_ID + " TEXT, " + Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, " + Message.FINGERPRINT + " TEXT, "
+ Message.CARBON + " INTEGER, " + Message.CARBON + " INTEGER, "
+ Message.EDITED + " TEXT, "
+ Message.READ + " NUMBER DEFAULT 1, " + Message.READ + " NUMBER DEFAULT 1, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES " + Message.CONVERSATION + ") REFERENCES "
@ -370,6 +371,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (oldVersion < 23 && newVersion >= 23) { if (oldVersion < 23 && newVersion >= 23) {
db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); 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) { public static synchronized DatabaseBackend getInstance(Context context) {
@ -586,6 +591,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ "=?", args); + "=?", 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) { public void readRoster(Roster roster) {
SQLiteDatabase db = this.getReadableDatabase(); SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor; Cursor cursor;

View file

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

View file

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

View file

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

View file

@ -123,6 +123,16 @@ public class MessageAdapter extends ArrayAdapter<Message> {
if (viewHolder.indicatorReceived != null) { if (viewHolder.indicatorReceived != null) {
viewHolder.indicatorReceived.setVisibility(View.GONE); 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 boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
&& message.getMergedStatus() <= Message.STATUS_RECEIVED; && message.getMergedStatus() <= Message.STATUS_RECEIVED;
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) { 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) { if (message.getEncryption() == Message.ENCRYPTION_NONE) {
viewHolder.indicator.setVisibility(View.GONE); viewHolder.indicator.setVisibility(View.GONE);
} else { } 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); viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
XmppAxolotlSession.Trust trust = message.getConversation() XmppAxolotlSession.Trust trust = message.getConversation()
@ -463,6 +473,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
.findViewById(R.id.download_button); .findViewById(R.id.download_button);
viewHolder.indicator = (ImageView) view viewHolder.indicator = (ImageView) view
.findViewById(R.id.security_indicator); .findViewById(R.id.security_indicator);
viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator);
viewHolder.image = (ImageView) view viewHolder.image = (ImageView) view
.findViewById(R.id.message_image); .findViewById(R.id.message_image);
viewHolder.messageBody = (TextView) view viewHolder.messageBody = (TextView) view
@ -483,6 +494,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
.findViewById(R.id.download_button); .findViewById(R.id.download_button);
viewHolder.indicator = (ImageView) view viewHolder.indicator = (ImageView) view
.findViewById(R.id.security_indicator); .findViewById(R.id.security_indicator);
viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator);
viewHolder.image = (ImageView) view viewHolder.image = (ImageView) view
.findViewById(R.id.message_image); .findViewById(R.id.message_image);
viewHolder.messageBody = (TextView) view viewHolder.messageBody = (TextView) view
@ -701,6 +713,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected TextView status_message; protected TextView status_message;
protected TextView encryption; protected TextView encryption;
public Button load_more_messages; public Button load_more_messages;
public ImageView edit_indicator;
} }
class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { 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:layout_marginRight="4sp"
android:alpha="0.70" android:alpha="0.70"
android:gravity="center_vertical" 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 <TextView
android:id="@+id/message_time" android:id="@+id/message_time"

View file

@ -91,7 +91,17 @@
android:layout_marginLeft="4sp" android:layout_marginLeft="4sp"
android:alpha="0.54" android:alpha="0.54"
android:gravity="center_vertical" 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 <ImageView
android:id="@+id/indicator_received" android:id="@+id/indicator_received"

View file

@ -5,6 +5,10 @@
android:id="@+id/copy_text" android:id="@+id/copy_text"
android:title="@string/copy_text" android:title="@string/copy_text"
android:visible="false"/> android:visible="false"/>
<item
android:id="@+id/correct_message"
android:title="@string/correct_message"
android:visible="false"/>
<item <item
android:id="@+id/share_with" android:id="@+id/share_with"
android:title="@string/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="selection_too_large">The selected area is too large</string>
<string name="no_accounts">(No activated accounts)</string> <string name="no_accounts">(No activated accounts)</string>
<string name="this_field_is_required">This field is required</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> </resources>