basic support for XEP-0308: Last Message Correction. fixes #864
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -167,7 +172,8 @@ public class Message extends AbstractEntity {
|
||||||
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,
|
||||||
|
|
|
@ -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"};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()) {
|
||||||
|
if (saveInDb) {
|
||||||
databaseBackend.createMessage(message);
|
databaseBackend.createMessage(message);
|
||||||
|
} else if (message.edited()) {
|
||||||
|
databaseBackend.updateMessage(message, message.getEditedId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateConversationUi();
|
updateConversationUi();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
if (conversation.getCorrectingMessage() != null) {
|
||||||
|
conversation.setCorrectingMessage(null);
|
||||||
|
mEditMessage.getEditableText().clear();
|
||||||
|
}
|
||||||
|
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||||
conversation.setNextCounterpart(null);
|
conversation.setNextCounterpart(null);
|
||||||
|
}
|
||||||
updateChatMsgHint();
|
updateChatMsgHint();
|
||||||
updateSendButton();
|
updateSendButton();
|
||||||
}
|
}
|
||||||
|
@ -330,13 +335,22 @@ 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.getCorrectingMessage() == null) {
|
||||||
|
message = new Message(conversation, body, conversation.getNextEncryption());
|
||||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||||
if (conversation.getNextCounterpart() != null) {
|
if (conversation.getNextCounterpart() != null) {
|
||||||
message.setCounterpart(conversation.getNextCounterpart());
|
message.setCounterpart(conversation.getNextCounterpart());
|
||||||
message.setType(Message.TYPE_PRIVATE);
|
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:
|
||||||
sendOtrMessage(message);
|
sendOtrMessage(message);
|
||||||
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
After Width: | Height: | Size: 368 B |
After Width: | Height: | Size: 371 B |
After Width: | Height: | Size: 264 B |
After Width: | Height: | Size: 299 B |
Before Width: | Height: | Size: 294 B |
Before Width: | Height: | Size: 322 B |
After Width: | Height: | Size: 293 B |
After Width: | Height: | Size: 298 B |
After Width: | Height: | Size: 229 B |
After Width: | Height: | Size: 249 B |
Before Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 306 B |
After Width: | Height: | Size: 397 B |
After Width: | Height: | Size: 399 B |
After Width: | Height: | Size: 291 B |
After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 410 B |
Before Width: | Height: | Size: 434 B |
After Width: | Height: | Size: 559 B |
After Width: | Height: | Size: 558 B |
After Width: | Height: | Size: 336 B |
After Width: | Height: | Size: 436 B |
Before Width: | Height: | Size: 380 B |
Before Width: | Height: | Size: 441 B |
After Width: | Height: | Size: 636 B |
After Width: | Height: | Size: 760 B |
After Width: | Height: | Size: 366 B |
After Width: | Height: | Size: 490 B |
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|