added typing notifications through XEP-0085. fixed #210

This commit is contained in:
iNPUTmice 2015-02-21 11:06:52 +01:00
parent 3f248e0d89
commit 7ee5e95959
14 changed files with 267 additions and 53 deletions

View File

@ -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",

View File

@ -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);
} }

View File

@ -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();

View File

@ -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;
} }

View File

@ -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"

View File

@ -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;
} }

View File

@ -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);

View File

@ -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);
} }

View File

@ -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);
}
}
} }

View File

@ -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();
} }
} }

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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