diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 5dc7c207..41e8971f 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -2,6 +2,8 @@ package eu.siacs.conversations; import android.graphics.Bitmap; +import eu.siacs.conversations.xmpp.chatstate.ChatState; + public final class Config { 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 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[] = { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java index d5c45465..c85864d0 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java @@ -21,6 +21,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.XmppConnectionService; 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.Jid; 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("no-copy", "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); account.getXmppConnection().sendMessagePacket(packet); } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 71c2523e..1feb04c7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -21,6 +21,7 @@ import java.util.Comparator; import java.util.List; 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.Jid; @@ -77,6 +78,8 @@ public class Conversation extends AbstractEntity implements Blockable { private Bookmark bookmark; private boolean messagesLeftOnServer = true; + private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE; + private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; public boolean hasMessagesLeftOnServer() { 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() { synchronized (this.messages) { final int size = messages.size(); diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index ce421ceb..ccf274b7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -147,10 +147,11 @@ public class Message extends AbstractEntity { 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.setType(Message.TYPE_STATUS); message.setConversation(conversation); + message.setBody(body); return message; } diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index ef1eda3f..79626511 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -27,7 +27,8 @@ public abstract class AbstractGenerator { "http://jabber.org/protocol/disco#info", "urn:xmpp:avatar:metadata+notify", "urn:xmpp:ping", - "jabber:iq:version"}; + "jabber:iq:version", + "http://jabber.org/protocol/chatstates"}; private final String[] MESSAGE_CONFIRMATION_FEATURES = { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 2ee636b5..e274d2bc 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -12,6 +12,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; 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.stanzas.MessagePacket; @@ -102,21 +103,12 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generateNotAcceptable(MessagePacket origin) { - MessagePacket packet = generateError(origin); - Element error = packet.addChild("error"); - error.setAttribute("type", "modify"); - error.setAttribute("code", "406"); - error.addChild("not-acceptable"); - return packet; - } - - private MessagePacket generateError(MessagePacket origin) { + public MessagePacket generateChatState(Conversation conversation) { + final Account account = conversation.getAccount(); MessagePacket packet = new MessagePacket(); - packet.setId(origin.getId()); - packet.setTo(origin.getFrom()); - packet.setBody(origin.getBody()); - packet.setType(MessagePacket.TYPE_ERROR); + packet.setTo(conversation.getJid().toBareJid()); + packet.setFrom(account.getJid()); + packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); return packet; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 49efb004..3f3cf8cb 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -1,8 +1,11 @@ package eu.siacs.conversations.parser; +import android.util.Log; + import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; 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.xml.Element; 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.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -24,6 +28,21 @@ public class MessageParser extends AbstractParser implements 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) { final Jid jid = packet.getFrom(); if (jid == null) { @@ -55,6 +74,7 @@ public class MessageParser extends AbstractParser implements } finishedMessage.setCounterpart(jid); finishedMessage.setTime(getTimestamp(packet)); + extractChatState(conversation,packet); return finishedMessage; } @@ -123,6 +143,7 @@ public class MessageParser extends AbstractParser implements finishedMessage.setRemoteMsgId(packet.getId()); finishedMessage.markable = isMarkable(packet); finishedMessage.setCounterpart(from); + extractChatState(conversation,packet); return finishedMessage; } catch (Exception e) { conversation.resetOtrSession(); @@ -275,6 +296,7 @@ public class MessageParser extends AbstractParser implements finishedMessage = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } + extractChatState(conversation,message); finishedMessage.setTime(getTimestamp(message)); finishedMessage.setRemoteMsgId(message.getAttribute("id")); finishedMessage.markable = isMarkable(message); @@ -362,6 +384,9 @@ public class MessageParser extends AbstractParser implements private void parseNonMessage(Element packet, Account account) { final Jid from = packet.getAttributeAsJid("from"); + if (extractChatState(from == null ? null : mXmppConnectionService.find(account,from), packet)) { + mXmppConnectionService.updateConversationUi(); + } Element invite = extractInvite(packet); if (invite != null) { Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, from, true); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 948e78d4..1d2def93 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -86,6 +86,7 @@ import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; 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.Field; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -603,6 +604,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return connection; } + public void sendChatState(Conversation conversation) { + if (sendChatStates()) { + MessagePacket packet = mMessageGenerator.generateChatState(conversation); + sendMessagePacket(conversation.getAccount(), packet); + } + } + public void sendMessage(final Message message) { final Account account = message.getConversation().getAccount(); account.deactivateGracePeriod(); @@ -703,6 +711,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } if ((send) && (packet != null)) { + if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (this.sendChatStates()) { + packet.addChild(ChatState.toElement(conv.getOutgoingChatState())); + } + } sendMessagePacket(account, packet); } updateConversationUi(); @@ -784,6 +797,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { 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); } } @@ -2046,6 +2064,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return getPreferences().getBoolean("confirm_messages", true); } + public boolean sendChatStates() { + return getPreferences().getBoolean("chat_states", false); + } + public boolean saveEncryptedMessages() { return !getPreferences().getBoolean("dont_save_encrypted", false); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 35599946..3e0668be 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -40,6 +40,7 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentLinkedQueue; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; 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.Presences; 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.OnValueEdited; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; -public class ConversationFragment extends Fragment { +public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener { protected Conversation conversation; private OnClickListener leaveMuc = new OnClickListener() { @@ -327,18 +328,6 @@ public class ConversationFragment extends Fragment { } }); 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.setOnClickListener(this.mSendButtonListener); @@ -558,7 +547,17 @@ public class ConversationFragment extends Fragment { mDecryptJobRunning = false; super.onStop(); 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) { return; } + + this.activity = (ConversationActivity) getActivity(); + 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.activity = (ConversationActivity) getActivity(); + this.askForPassphraseIntent = null; this.conversation = conversation; this.mDecryptJobRunning = false; @@ -578,8 +584,10 @@ public class ConversationFragment extends Fragment { if (this.conversation.getMode() == Conversation.MODE_MULTI) { this.conversation.setNextCounterpart(null); } + this.mEditMessage.setKeyboardListener(null); this.mEditMessage.setText(""); this.mEditMessage.append(this.conversation.getNextMessage()); + this.mEditMessage.setKeyboardListener(this); this.messagesView.setAdapter(messageListAdapter); updateMessages(); this.messagesLoaded = true; @@ -834,13 +842,21 @@ public class ConversationFragment extends Fragment { protected void updateStatusMessages() { synchronized (this.messageList) { if (conversation.getMode() == Conversation.MODE_SINGLE) { - for (int i = this.messageList.size() - 1; i >= 0; --i) { - if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { - return; - } else { - if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { - this.messageList.add(i + 1,Message.createStatusMessage(conversation)); + 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) { + if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { return; + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + this.messageList.add(i + 1, + Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); + return; + } } } } @@ -995,4 +1011,33 @@ public class ConversationFragment extends Fragment { 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); + } + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java index 5090bbf5..57fa7285 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -1,10 +1,13 @@ package eu.siacs.conversations.ui; import android.content.Context; +import android.os.Handler; import android.util.AttributeSet; import android.view.KeyEvent; import android.widget.EditText; +import eu.siacs.conversations.Config; + public class EditMessage extends EditText { public EditMessage(Context context, AttributeSet attrs) { @@ -15,28 +18,62 @@ public class EditMessage extends EditText { 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 public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_ENTER) { - if (mOnEnterPressed != null) { - if (mOnEnterPressed.onEnterPressed()) { - return true; - } else { - return super.onKeyDown(keyCode, event); - } + if (keyboardListener != null) { + keyboardListener.onEnterPressed(); } + return true; } return super.onKeyDown(keyCode, event); } - public void setOnEnterPressedListener(OnEnterPressed listener) { - this.mOnEnterPressed = listener; + @Override + 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 boolean onEnterPressed(); + public void setKeyboardListener(KeyboardListener listener) { + this.keyboardListener = listener; + if (listener != null) { + this.isUserTyping = false; + } + } + + public interface KeyboardListener { + public void onEnterPressed(); + public void onTypingStarted(); + public void onTypingStopped(); + public void onTextDeleted(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 442c6ecf..58713534 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -410,9 +410,7 @@ public class MessageAdapter extends ArrayAdapter { .avatarService().get(conversation.getContact(), activity.getPixel(32))); viewHolder.contact_picture.setAlpha(0.5f); - viewHolder.status_message.setText( - activity.getString(R.string.contact_has_read_up_to_this_point, conversation.getName())); - + viewHolder.status_message.setText(message.getBody()); } return view; } else if (type == NULL) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java new file mode 100644 index 00000000..f85efbdb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java @@ -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; + } +} diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ebf25604..a17859fc 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -445,4 +445,8 @@ Offering %s Hide offline Disable Account + %s is typing... + %s has stopped typing + Typing notifications + Let your contact know when you are writing a new message diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 7c92530f..8bf6eb87 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -28,6 +28,13 @@ android:key="confirm_messages" android:summary="@string/pref_confirm_messages_summary" android:title="@string/pref_confirm_messages" /> + + +