added typing notifications through XEP-0085. fixed #210
This commit is contained in:
parent
3f248e0d89
commit
7ee5e95959
|
@ -2,6 +2,8 @@ package eu.siacs.conversations;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
|
|
||||||
public final class Config {
|
public final class Config {
|
||||||
|
|
||||||
public static final String LOGTAG = "conversations";
|
public static final String LOGTAG = "conversations";
|
||||||
|
@ -30,6 +32,9 @@ public final class Config {
|
||||||
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
|
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
|
||||||
public static final int MAM_MAX_MESSAGES = 500;
|
public static final int MAM_MAX_MESSAGES = 500;
|
||||||
|
|
||||||
|
public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
|
||||||
|
public static final int TYPING_TIMEOUT = 8;
|
||||||
|
|
||||||
public static final String ENABLED_CIPHERS[] = {
|
public static final String ENABLED_CIPHERS[] = {
|
||||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",
|
||||||
|
|
|
@ -21,6 +21,7 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
@ -182,6 +183,19 @@ public class OtrEngine extends OtrCryptoEngineImpl implements OtrEngineHost {
|
||||||
packet.addChild("private", "urn:xmpp:carbons:2");
|
packet.addChild("private", "urn:xmpp:carbons:2");
|
||||||
packet.addChild("no-copy", "urn:xmpp:hints");
|
packet.addChild("no-copy", "urn:xmpp:hints");
|
||||||
packet.addChild("no-store", "urn:xmpp:hints");
|
packet.addChild("no-store", "urn:xmpp:hints");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Jid jid = Jid.fromSessionID(session);
|
||||||
|
Conversation conversation = mXmppConnectionService.find(account,jid);
|
||||||
|
if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
||||||
|
if (mXmppConnectionService.sendChatStates()) {
|
||||||
|
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final InvalidJidException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
packet.setType(MessagePacket.TYPE_CHAT);
|
packet.setType(MessagePacket.TYPE_CHAT);
|
||||||
account.getXmppConnection().sendMessagePacket(packet);
|
account.getXmppConnection().sendMessagePacket(packet);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||||
|
|
||||||
|
@ -77,6 +78,8 @@ public class Conversation extends AbstractEntity implements Blockable {
|
||||||
private Bookmark bookmark;
|
private Bookmark bookmark;
|
||||||
|
|
||||||
private boolean messagesLeftOnServer = true;
|
private boolean messagesLeftOnServer = true;
|
||||||
|
private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
|
||||||
|
private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
|
||||||
|
|
||||||
public boolean hasMessagesLeftOnServer() {
|
public boolean hasMessagesLeftOnServer() {
|
||||||
return messagesLeftOnServer;
|
return messagesLeftOnServer;
|
||||||
|
@ -138,6 +141,34 @@ public class Conversation extends AbstractEntity implements Blockable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean setIncomingChatState(ChatState state) {
|
||||||
|
if (this.mIncomingChatState == state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.mIncomingChatState = state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatState getIncomingChatState() {
|
||||||
|
return this.mIncomingChatState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setOutgoingChatState(ChatState state) {
|
||||||
|
if (mode == MODE_MULTI) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.mOutgoingChatState != state) {
|
||||||
|
this.mOutgoingChatState = state;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatState getOutgoingChatState() {
|
||||||
|
return this.mOutgoingChatState;
|
||||||
|
}
|
||||||
|
|
||||||
public void trim() {
|
public void trim() {
|
||||||
synchronized (this.messages) {
|
synchronized (this.messages) {
|
||||||
final int size = messages.size();
|
final int size = messages.size();
|
||||||
|
|
|
@ -147,10 +147,11 @@ public class Message extends AbstractEntity {
|
||||||
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
|
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Message createStatusMessage(Conversation conversation) {
|
public static Message createStatusMessage(Conversation conversation, String body) {
|
||||||
Message message = new Message();
|
Message message = new Message();
|
||||||
message.setType(Message.TYPE_STATUS);
|
message.setType(Message.TYPE_STATUS);
|
||||||
message.setConversation(conversation);
|
message.setConversation(conversation);
|
||||||
|
message.setBody(body);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,8 @@ public abstract class AbstractGenerator {
|
||||||
"http://jabber.org/protocol/disco#info",
|
"http://jabber.org/protocol/disco#info",
|
||||||
"urn:xmpp:avatar:metadata+notify",
|
"urn:xmpp:avatar:metadata+notify",
|
||||||
"urn:xmpp:ping",
|
"urn:xmpp:ping",
|
||||||
"jabber:iq:version"};
|
"jabber:iq:version",
|
||||||
|
"http://jabber.org/protocol/chatstates"};
|
||||||
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
|
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
|
||||||
"urn:xmpp:chat-markers:0",
|
"urn:xmpp:chat-markers:0",
|
||||||
"urn:xmpp:receipts"
|
"urn:xmpp:receipts"
|
||||||
|
|
|
@ -12,6 +12,7 @@ import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
|
||||||
|
@ -102,21 +103,12 @@ public class MessageGenerator extends AbstractGenerator {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessagePacket generateNotAcceptable(MessagePacket origin) {
|
public MessagePacket generateChatState(Conversation conversation) {
|
||||||
MessagePacket packet = generateError(origin);
|
final Account account = conversation.getAccount();
|
||||||
Element error = packet.addChild("error");
|
|
||||||
error.setAttribute("type", "modify");
|
|
||||||
error.setAttribute("code", "406");
|
|
||||||
error.addChild("not-acceptable");
|
|
||||||
return packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MessagePacket generateError(MessagePacket origin) {
|
|
||||||
MessagePacket packet = new MessagePacket();
|
MessagePacket packet = new MessagePacket();
|
||||||
packet.setId(origin.getId());
|
packet.setTo(conversation.getJid().toBareJid());
|
||||||
packet.setTo(origin.getFrom());
|
packet.setFrom(account.getJid());
|
||||||
packet.setBody(origin.getBody());
|
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
|
||||||
packet.setType(MessagePacket.TYPE_ERROR);
|
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package eu.siacs.conversations.parser;
|
package eu.siacs.conversations.parser;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import net.java.otr4j.session.Session;
|
import net.java.otr4j.session.Session;
|
||||||
import net.java.otr4j.session.SessionStatus;
|
import net.java.otr4j.session.SessionStatus;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
|
@ -14,6 +17,7 @@ import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
|
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||||
import eu.siacs.conversations.xmpp.pep.Avatar;
|
import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
@ -24,6 +28,21 @@ public class MessageParser extends AbstractParser implements
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean extractChatState(Conversation conversation, final Element element) {
|
||||||
|
ChatState state = ChatState.parse(element);
|
||||||
|
if (state != null && conversation != null) {
|
||||||
|
final Account account = conversation.getAccount();
|
||||||
|
Jid from = element.getAttributeAsJid("from");
|
||||||
|
if (from != null && from.toBareJid().equals(account.getJid().toBareJid())) {
|
||||||
|
conversation.setOutgoingChatState(state);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return conversation.setIncomingChatState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private Message parseChat(MessagePacket packet, Account account) {
|
private Message parseChat(MessagePacket packet, Account account) {
|
||||||
final Jid jid = packet.getFrom();
|
final Jid jid = packet.getFrom();
|
||||||
if (jid == null) {
|
if (jid == null) {
|
||||||
|
@ -55,6 +74,7 @@ public class MessageParser extends AbstractParser implements
|
||||||
}
|
}
|
||||||
finishedMessage.setCounterpart(jid);
|
finishedMessage.setCounterpart(jid);
|
||||||
finishedMessage.setTime(getTimestamp(packet));
|
finishedMessage.setTime(getTimestamp(packet));
|
||||||
|
extractChatState(conversation,packet);
|
||||||
return finishedMessage;
|
return finishedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +143,7 @@ public class MessageParser extends AbstractParser implements
|
||||||
finishedMessage.setRemoteMsgId(packet.getId());
|
finishedMessage.setRemoteMsgId(packet.getId());
|
||||||
finishedMessage.markable = isMarkable(packet);
|
finishedMessage.markable = isMarkable(packet);
|
||||||
finishedMessage.setCounterpart(from);
|
finishedMessage.setCounterpart(from);
|
||||||
|
extractChatState(conversation,packet);
|
||||||
return finishedMessage;
|
return finishedMessage;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
conversation.resetOtrSession();
|
conversation.resetOtrSession();
|
||||||
|
@ -275,6 +296,7 @@ public class MessageParser extends AbstractParser implements
|
||||||
finishedMessage = new Message(conversation, body,
|
finishedMessage = new Message(conversation, body,
|
||||||
Message.ENCRYPTION_NONE, status);
|
Message.ENCRYPTION_NONE, status);
|
||||||
}
|
}
|
||||||
|
extractChatState(conversation,message);
|
||||||
finishedMessage.setTime(getTimestamp(message));
|
finishedMessage.setTime(getTimestamp(message));
|
||||||
finishedMessage.setRemoteMsgId(message.getAttribute("id"));
|
finishedMessage.setRemoteMsgId(message.getAttribute("id"));
|
||||||
finishedMessage.markable = isMarkable(message);
|
finishedMessage.markable = isMarkable(message);
|
||||||
|
@ -362,6 +384,9 @@ public class MessageParser extends AbstractParser implements
|
||||||
|
|
||||||
private void parseNonMessage(Element packet, Account account) {
|
private void parseNonMessage(Element packet, Account account) {
|
||||||
final Jid from = packet.getAttributeAsJid("from");
|
final Jid from = packet.getAttributeAsJid("from");
|
||||||
|
if (extractChatState(from == null ? null : mXmppConnectionService.find(account,from), packet)) {
|
||||||
|
mXmppConnectionService.updateConversationUi();
|
||||||
|
}
|
||||||
Element invite = extractInvite(packet);
|
Element invite = extractInvite(packet);
|
||||||
if (invite != null) {
|
if (invite != null) {
|
||||||
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, from, true);
|
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, from, true);
|
||||||
|
|
|
@ -86,6 +86,7 @@ import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
|
||||||
import eu.siacs.conversations.xmpp.OnStatusChanged;
|
import eu.siacs.conversations.xmpp.OnStatusChanged;
|
||||||
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
||||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.forms.Data;
|
import eu.siacs.conversations.xmpp.forms.Data;
|
||||||
import eu.siacs.conversations.xmpp.forms.Field;
|
import eu.siacs.conversations.xmpp.forms.Field;
|
||||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||||
|
@ -603,6 +604,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendChatState(Conversation conversation) {
|
||||||
|
if (sendChatStates()) {
|
||||||
|
MessagePacket packet = mMessageGenerator.generateChatState(conversation);
|
||||||
|
sendMessagePacket(conversation.getAccount(), packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void sendMessage(final Message message) {
|
public void sendMessage(final Message message) {
|
||||||
final Account account = message.getConversation().getAccount();
|
final Account account = message.getConversation().getAccount();
|
||||||
account.deactivateGracePeriod();
|
account.deactivateGracePeriod();
|
||||||
|
@ -703,6 +711,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((send) && (packet != null)) {
|
if ((send) && (packet != null)) {
|
||||||
|
if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
||||||
|
if (this.sendChatStates()) {
|
||||||
|
packet.addChild(ChatState.toElement(conv.getOutgoingChatState()));
|
||||||
|
}
|
||||||
|
}
|
||||||
sendMessagePacket(account, packet);
|
sendMessagePacket(account, packet);
|
||||||
}
|
}
|
||||||
updateConversationUi();
|
updateConversationUi();
|
||||||
|
@ -784,6 +797,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
|
||||||
} else {
|
} else {
|
||||||
markMessage(message, Message.STATUS_UNSEND);
|
markMessage(message, Message.STATUS_UNSEND);
|
||||||
}
|
}
|
||||||
|
if (message.getConversation().setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
||||||
|
if (this.sendChatStates()) {
|
||||||
|
packet.addChild(ChatState.toElement(message.getConversation().getOutgoingChatState()));
|
||||||
|
}
|
||||||
|
}
|
||||||
sendMessagePacket(account, packet);
|
sendMessagePacket(account, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2046,6 +2064,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
|
||||||
return getPreferences().getBoolean("confirm_messages", true);
|
return getPreferences().getBoolean("confirm_messages", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean sendChatStates() {
|
||||||
|
return getPreferences().getBoolean("chat_states", false);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean saveEncryptedMessages() {
|
public boolean saveEncryptedMessages() {
|
||||||
return !getPreferences().getBoolean("dont_save_encrypted", false);
|
return !getPreferences().getBoolean("dont_save_encrypted", false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import java.util.List;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.crypto.PgpEngine;
|
import eu.siacs.conversations.crypto.PgpEngine;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
@ -52,15 +53,15 @@ import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.MucOptions;
|
import eu.siacs.conversations.entities.MucOptions;
|
||||||
import eu.siacs.conversations.entities.Presences;
|
import eu.siacs.conversations.entities.Presences;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.ui.EditMessage.OnEnterPressed;
|
|
||||||
import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
|
import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
|
||||||
import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
|
import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
|
||||||
import eu.siacs.conversations.ui.adapter.MessageAdapter;
|
import eu.siacs.conversations.ui.adapter.MessageAdapter;
|
||||||
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
|
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
|
||||||
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
|
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||||
|
|
||||||
public class ConversationFragment extends Fragment {
|
public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
|
||||||
|
|
||||||
protected Conversation conversation;
|
protected Conversation conversation;
|
||||||
private OnClickListener leaveMuc = new OnClickListener() {
|
private OnClickListener leaveMuc = new OnClickListener() {
|
||||||
|
@ -327,18 +328,6 @@ public class ConversationFragment extends Fragment {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mEditMessage.setOnEditorActionListener(mEditorActionListener);
|
mEditMessage.setOnEditorActionListener(mEditorActionListener);
|
||||||
mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onEnterPressed() {
|
|
||||||
if (activity.enterIsSend()) {
|
|
||||||
sendMessage();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
|
mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
|
||||||
mSendButton.setOnClickListener(this.mSendButtonListener);
|
mSendButton.setOnClickListener(this.mSendButtonListener);
|
||||||
|
@ -558,7 +547,17 @@ public class ConversationFragment extends Fragment {
|
||||||
mDecryptJobRunning = false;
|
mDecryptJobRunning = false;
|
||||||
super.onStop();
|
super.onStop();
|
||||||
if (this.conversation != null) {
|
if (this.conversation != null) {
|
||||||
this.conversation.setNextMessage(mEditMessage.getText().toString());
|
final String msg = mEditMessage.getText().toString();
|
||||||
|
this.conversation.setNextMessage(msg);
|
||||||
|
updateChatState(this.conversation,msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChatState(final Conversation conversation, final String msg) {
|
||||||
|
ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
|
||||||
|
Account.State status = conversation.getAccount().getStatus();
|
||||||
|
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
|
||||||
|
activity.xmppConnectionService.sendChatState(conversation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,11 +565,18 @@ public class ConversationFragment extends Fragment {
|
||||||
if (conversation == null) {
|
if (conversation == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.activity = (ConversationActivity) getActivity();
|
||||||
|
|
||||||
if (this.conversation != null) {
|
if (this.conversation != null) {
|
||||||
this.conversation.setNextMessage(mEditMessage.getText().toString());
|
final String msg = mEditMessage.getText().toString();
|
||||||
|
this.conversation.setNextMessage(msg);
|
||||||
|
if (this.conversation != conversation) {
|
||||||
|
updateChatState(this.conversation,msg);
|
||||||
|
}
|
||||||
this.conversation.trim();
|
this.conversation.trim();
|
||||||
}
|
}
|
||||||
this.activity = (ConversationActivity) getActivity();
|
|
||||||
this.askForPassphraseIntent = null;
|
this.askForPassphraseIntent = null;
|
||||||
this.conversation = conversation;
|
this.conversation = conversation;
|
||||||
this.mDecryptJobRunning = false;
|
this.mDecryptJobRunning = false;
|
||||||
|
@ -578,8 +584,10 @@ public class ConversationFragment extends Fragment {
|
||||||
if (this.conversation.getMode() == Conversation.MODE_MULTI) {
|
if (this.conversation.getMode() == Conversation.MODE_MULTI) {
|
||||||
this.conversation.setNextCounterpart(null);
|
this.conversation.setNextCounterpart(null);
|
||||||
}
|
}
|
||||||
|
this.mEditMessage.setKeyboardListener(null);
|
||||||
this.mEditMessage.setText("");
|
this.mEditMessage.setText("");
|
||||||
this.mEditMessage.append(this.conversation.getNextMessage());
|
this.mEditMessage.append(this.conversation.getNextMessage());
|
||||||
|
this.mEditMessage.setKeyboardListener(this);
|
||||||
this.messagesView.setAdapter(messageListAdapter);
|
this.messagesView.setAdapter(messageListAdapter);
|
||||||
updateMessages();
|
updateMessages();
|
||||||
this.messagesLoaded = true;
|
this.messagesLoaded = true;
|
||||||
|
@ -834,12 +842,19 @@ public class ConversationFragment extends Fragment {
|
||||||
protected void updateStatusMessages() {
|
protected void updateStatusMessages() {
|
||||||
synchronized (this.messageList) {
|
synchronized (this.messageList) {
|
||||||
if (conversation.getMode() == Conversation.MODE_SINGLE) {
|
if (conversation.getMode() == Conversation.MODE_SINGLE) {
|
||||||
|
ChatState state = conversation.getIncomingChatState();
|
||||||
|
if (state == ChatState.COMPOSING) {
|
||||||
|
this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
|
||||||
|
} else if (state == ChatState.PAUSED) {
|
||||||
|
this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
|
||||||
|
} else {
|
||||||
for (int i = this.messageList.size() - 1; i >= 0; --i) {
|
for (int i = this.messageList.size() - 1; i >= 0; --i) {
|
||||||
if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
|
if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
|
if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
|
||||||
this.messageList.add(i + 1,Message.createStatusMessage(conversation));
|
this.messageList.add(i + 1,
|
||||||
|
Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -847,6 +862,7 @@ public class ConversationFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void makeFingerprintWarning() {
|
protected void makeFingerprintWarning() {
|
||||||
|
|
||||||
|
@ -995,4 +1011,33 @@ public class ConversationFragment extends Fragment {
|
||||||
this.mEditMessage.append(text);
|
this.mEditMessage.append(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnterPressed() {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTypingStarted() {
|
||||||
|
Account.State status = conversation.getAccount().getStatus();
|
||||||
|
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
|
||||||
|
activity.xmppConnectionService.sendChatState(conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTypingStopped() {
|
||||||
|
Account.State status = conversation.getAccount().getStatus();
|
||||||
|
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
|
||||||
|
activity.xmppConnectionService.sendChatState(conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextDeleted() {
|
||||||
|
Account.State status = conversation.getAccount().getStatus();
|
||||||
|
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
||||||
|
activity.xmppConnectionService.sendChatState(conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package eu.siacs.conversations.ui;
|
package eu.siacs.conversations.ui;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
|
||||||
public class EditMessage extends EditText {
|
public class EditMessage extends EditText {
|
||||||
|
|
||||||
public EditMessage(Context context, AttributeSet attrs) {
|
public EditMessage(Context context, AttributeSet attrs) {
|
||||||
|
@ -15,28 +18,62 @@ public class EditMessage extends EditText {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected OnEnterPressed mOnEnterPressed;
|
protected Handler mTypingHandler = new Handler();
|
||||||
|
|
||||||
|
protected Runnable mTypingTimeout = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isUserTyping && keyboardListener != null) {
|
||||||
|
keyboardListener.onTypingStopped();
|
||||||
|
isUserTyping = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private boolean isUserTyping = false;
|
||||||
|
|
||||||
|
protected KeyboardListener keyboardListener;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||||
if (mOnEnterPressed != null) {
|
if (keyboardListener != null) {
|
||||||
if (mOnEnterPressed.onEnterPressed()) {
|
keyboardListener.onEnterPressed();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnEnterPressedListener(OnEnterPressed listener) {
|
@Override
|
||||||
this.mOnEnterPressed = listener;
|
public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
|
||||||
|
super.onTextChanged(text,start,lengthBefore,lengthAfter);
|
||||||
|
if (this.mTypingHandler != null && this.keyboardListener != null) {
|
||||||
|
this.mTypingHandler.removeCallbacks(mTypingTimeout);
|
||||||
|
this.mTypingHandler.postDelayed(mTypingTimeout, Config.TYPING_TIMEOUT * 1000);
|
||||||
|
final int length = text.length();
|
||||||
|
if (!isUserTyping && length > 0) {
|
||||||
|
this.isUserTyping = true;
|
||||||
|
this.keyboardListener.onTypingStarted();
|
||||||
|
} else if (length == 0) {
|
||||||
|
this.isUserTyping = false;
|
||||||
|
this.keyboardListener.onTextDeleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnEnterPressed {
|
public void setKeyboardListener(KeyboardListener listener) {
|
||||||
public boolean onEnterPressed();
|
this.keyboardListener = listener;
|
||||||
|
if (listener != null) {
|
||||||
|
this.isUserTyping = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface KeyboardListener {
|
||||||
|
public void onEnterPressed();
|
||||||
|
public void onTypingStarted();
|
||||||
|
public void onTypingStopped();
|
||||||
|
public void onTextDeleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -410,9 +410,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
.avatarService().get(conversation.getContact(),
|
.avatarService().get(conversation.getContact(),
|
||||||
activity.getPixel(32)));
|
activity.getPixel(32)));
|
||||||
viewHolder.contact_picture.setAlpha(0.5f);
|
viewHolder.contact_picture.setAlpha(0.5f);
|
||||||
viewHolder.status_message.setText(
|
viewHolder.status_message.setText(message.getBody());
|
||||||
activity.getString(R.string.contact_has_read_up_to_this_point, conversation.getName()));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return view;
|
return view;
|
||||||
} else if (type == NULL) {
|
} else if (type == NULL) {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package eu.siacs.conversations.xmpp.chatstate;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
|
||||||
|
public enum ChatState {
|
||||||
|
|
||||||
|
ACTIVE, INACTIVE, GONE, COMPOSING, PAUSED, mIncomingChatState;
|
||||||
|
|
||||||
|
public static ChatState parse(Element element) {
|
||||||
|
final String NAMESPACE = "http://jabber.org/protocol/chatstates";
|
||||||
|
if (element.hasChild("active",NAMESPACE)) {
|
||||||
|
return ACTIVE;
|
||||||
|
} else if (element.hasChild("inactive",NAMESPACE)) {
|
||||||
|
return INACTIVE;
|
||||||
|
} else if (element.hasChild("composing",NAMESPACE)) {
|
||||||
|
return COMPOSING;
|
||||||
|
} else if (element.hasChild("gone",NAMESPACE)) {
|
||||||
|
return GONE;
|
||||||
|
} else if (element.hasChild("paused",NAMESPACE)) {
|
||||||
|
return PAUSED;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Element toElement(ChatState state) {
|
||||||
|
final String NAMESPACE = "http://jabber.org/protocol/chatstates";
|
||||||
|
final Element element = new Element(state.toString().toLowerCase());
|
||||||
|
element.setAttribute("xmlns",NAMESPACE);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
|
@ -445,4 +445,8 @@
|
||||||
<string name="offering_x_file">Offering %s</string>
|
<string name="offering_x_file">Offering %s</string>
|
||||||
<string name="hide_offline">Hide offline</string>
|
<string name="hide_offline">Hide offline</string>
|
||||||
<string name="disable_account">Disable Account</string>
|
<string name="disable_account">Disable Account</string>
|
||||||
|
<string name="contact_is_typing">%s is typing...</string>
|
||||||
|
<string name="contact_has_stopped_typing">%s has stopped typing</string>
|
||||||
|
<string name="pref_chat_states">Typing notifications</string>
|
||||||
|
<string name="pref_chat_states_summary">Let your contact know when you are writing a new message</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -28,6 +28,13 @@
|
||||||
android:key="confirm_messages"
|
android:key="confirm_messages"
|
||||||
android:summary="@string/pref_confirm_messages_summary"
|
android:summary="@string/pref_confirm_messages_summary"
|
||||||
android:title="@string/pref_confirm_messages" />
|
android:title="@string/pref_confirm_messages" />
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="chat_states"
|
||||||
|
android:summary="@string/pref_chat_states_summary"
|
||||||
|
android:title="@string/pref_chat_states" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory android:title="@string/pref_notification_settings" >
|
<PreferenceCategory android:title="@string/pref_notification_settings" >
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
|
|
Loading…
Reference in a new issue