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 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", | ||||
|  | ||||
| @ -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); | ||||
| 	} | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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; | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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; | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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); | ||||
| 	} | ||||
|  | ||||
| @ -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); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -410,9 +410,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { | ||||
| 						.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) { | ||||
|  | ||||
| @ -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="hide_offline">Hide offline</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> | ||||
|  | ||||
| @ -28,6 +28,13 @@ | ||||
|             android:key="confirm_messages" | ||||
|             android:summary="@string/pref_confirm_messages_summary" | ||||
|             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 android:title="@string/pref_notification_settings" > | ||||
|         <CheckBoxPreference | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 iNPUTmice
						iNPUTmice