/**
* $RCSfile: ,v $
* $Revision: $
* $Date: $
*
* Copyright (C) 2004-2011 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.spark.ui.rooms;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.TimerTask;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import org.jivesoftware.resource.Default;
import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromMatchesFilter;
import org.jivesoftware.smack.filter.OrFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.StreamError;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.ChatState;
import org.jivesoftware.smackx.ChatStateManager;
import org.jivesoftware.smackx.MessageEventManager;
import org.jivesoftware.smackx.packet.MessageEvent;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.PresenceManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.ui.ChatRoom;
import org.jivesoftware.spark.ui.ChatRoomButton;
import org.jivesoftware.spark.ui.ChatStatePanel;
import org.jivesoftware.spark.ui.ContactItem;
import org.jivesoftware.spark.ui.ContactList;
import org.jivesoftware.spark.ui.MessageEventListener;
import org.jivesoftware.spark.ui.RosterDialog;
import org.jivesoftware.spark.ui.VCardPanel;
import org.jivesoftware.spark.util.ModelUtil;
import org.jivesoftware.spark.util.TaskEngine;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.transcripts.ChatTranscript;
import org.jivesoftware.sparkimpl.plugin.transcripts.ChatTranscripts;
import org.jivesoftware.sparkimpl.plugin.transcripts.HistoryMessage;
import org.jivesoftware.sparkimpl.profile.VCardManager;
import org.jivesoftware.sparkimpl.settings.local.LocalPreferences;
import org.jivesoftware.sparkimpl.settings.local.SettingsManager;
/**
* This is the Person to Person implementation of <code>ChatRoom</code>
* This room only allows for 1 to 1 conversations.
*/
public class ChatRoomImpl extends ChatRoom {
private static final long serialVersionUID = 6163762803773980872L;
private List<MessageEventListener> messageEventListeners = new ArrayList<MessageEventListener>();
private String roomname;
private Icon tabIcon;
private String roomTitle;
private String tabTitle;
private String participantJID;
private String participantNickname;
private final Color TRANSPARENT_COLOR = new Color(0,0,0,0);
private Presence presence;
private boolean offlineSent;
private Roster roster;
private long startNotificationSendingTime;
private boolean sendComposingNotification = true;
private boolean sendPausingNotification = false;
private boolean sendInactiveNotification = false;
private boolean sendGoneNotification = false;
private ChatState lastNotificationSent = null;
private TimerTask typingTimerTask;
private boolean sendChatStateNotification = false;
private String threadID;
private long lastActivity;
private boolean active;
// Information button
private ChatRoomButton infoButton;
private ChatRoomButton addToRosterButton;
private VCardPanel vcardPanel;
private long pauseTimePeriod = 2000;
private long inactiveTimePeriod = 120000;
private long goneTimePeriod = 600000;
private JComponent chatStatePanel;
public ChatRoomImpl(final String participantJID, String participantNickname, String title) {
this(participantJID, participantNickname, title, true);
}
/**
* Constructs a 1-to-1 ChatRoom.
*
* @param participantJID the participants jid to chat with.
* @param participantNickname the nickname of the participant.
* @param title the title of the room.
*/
public ChatRoomImpl(final String participantJID, String participantNickname, String title, boolean initUi) {
this.active = true;
//activateNotificationTime = System.currentTimeMillis();
this.participantJID = participantJID;
this.participantNickname = participantNickname;
// Loads the current history for this user.
loadHistory();
// Register PacketListeners
PacketFilter fromFilter = new FromMatchesFilter(participantJID);
PacketFilter orFilter = new OrFilter(new PacketTypeFilter(Presence.class), new PacketTypeFilter(Message.class));
PacketFilter andFilter = new AndFilter(orFilter, fromFilter);
SparkManager.getConnection().addPacketListener(this, andFilter);
// The roomname will be the participantJID
this.roomname = participantJID;
// Use the agents username as the Tab Title
this.tabTitle = title;
// The name of the room will be the node of the user jid + conversation.
this.roomTitle = participantNickname;
// Add RoomInfo
this.getSplitPane().setRightComponent(null);
getSplitPane().setDividerSize(0);
presence = PresenceManager.getPresence(participantJID);
roster = SparkManager.getConnection().getRoster();
RosterEntry entry = roster.getEntry(participantJID);
tabIcon = PresenceManager.getIconFromPresence(presence);
if (initUi) {
// Create toolbar buttons.
infoButton = new ChatRoomButton("", SparkRes.getImageIcon(SparkRes.PROFILE_IMAGE_24x24));
infoButton.setToolTipText(Res.getString("message.view.information.about.this.user"));
// Create basic toolbar.
addChatRoomButton(infoButton);
// Show VCard.
infoButton.addActionListener(this);
}
// If the user is not in the roster, then allow user to add them.
addToRosterButton = new ChatRoomButton("", SparkRes.getImageIcon(SparkRes.ADD_IMAGE_24x24));
if (entry == null && !StringUtils.parseResource(participantJID).equals(participantNickname)) {
addToRosterButton.setToolTipText(Res.getString("message.add.this.user.to.your.roster"));
if(!Default.getBoolean(Default.ADD_CONTACT_DISABLED)) {
addChatRoomButton(addToRosterButton);
}
addToRosterButton.addActionListener(this);
}
// If this is a private chat from a group chat room, do not show toolbar.
if (StringUtils.parseResource(participantJID).equals(participantNickname)) {
getToolBar().setVisible(false);
}
createChatStateTimerTask();
lastActivity = System.currentTimeMillis();
getChatInputEditor().addFocusListener(new ChatStateFocusListener());
}
protected void createChatStateTimerTask() {
typingTimerTask = new TimerTask() {
public void run() {
if (!sendChatStateNotification) {
return;
}
long now = System.currentTimeMillis();
long time = now - startNotificationSendingTime;
if (inBetween(time, pauseTimePeriod, inactiveTimePeriod)) {
if (sendPausingNotification) {
// send cancel
//SparkManager.getMessageEventManager().sendCancelledNotification(getParticipantJID(), threadID);
sendChatState(ChatState.paused);
sendPausingNotification = false;
sendComposingNotification = true;
}
} else if (inBetween(time, inactiveTimePeriod, goneTimePeriod)) {
if(sendInactiveNotification) {
sendChatState(ChatState.inactive);
sendInactiveNotification = false;
}
} else if (time > goneTimePeriod) {
if (sendGoneNotification) {
sendChatState(ChatState.gone);
sendGoneNotification = false;
}
}
}
};
TaskEngine.getInstance().scheduleAtFixedRate(typingTimerTask, pauseTimePeriod, pauseTimePeriod);
}
private boolean inBetween(long time, long lowLimit, long highLimit) {
return lowLimit < time && time < highLimit;
}
public void activateChatStateNotificationSystem() {
startNotificationSendingTime = System.currentTimeMillis();
sendInactiveNotification = true;
sendGoneNotification = true;
sendPausingNotification = false;
sendComposingNotification = true;
if (lastNotificationSent == null || !lastNotificationSent.equals(ChatState.active)) {
sendChatState(ChatState.active);
}
}
public void inactivateChatStateNotificationSystem() {
startNotificationSendingTime = System.currentTimeMillis();
sendInactiveNotification = false;
sendGoneNotification = true;
sendPausingNotification = false;
sendComposingNotification = false;
if (lastNotificationSent != null && !lastNotificationSent.equals(ChatState.inactive) && !lastNotificationSent.equals(ChatState.gone)) {
sendChatState(ChatState.inactive);
}
}
private void sendChatState(ChatState state) {
Connection connection = SparkManager.getConnection();
boolean connected = connection.isConnected();
if (connected) {
Chat chat = connection.getChatManager().createChat(getParticipantJID(), null);
try {
ChatStateManager.getInstance(connection).setCurrentState(state, chat);
lastNotificationSent = state;
} catch (XMPPException e) {
Log.error("Cannot send " + state + " chat notification");
}
}
}
public void closeChatRoom() {
// If already closed, don't bother.
if (!active) {
return;
}
super.closeChatRoom();
removeListeners();
sendChatState(ChatState.gone);
sendGoneNotification = false;
SparkManager.getChatManager().removeChat(this);
SparkManager.getConnection().removePacketListener(this);
if (typingTimerTask != null) {
TaskEngine.getInstance().cancelScheduledTask(typingTimerTask);
typingTimerTask = null;
}
active = false;
vcardPanel = null;
this.removeAll();
}
protected void removeListeners() {
// Remove info listener
infoButton.removeActionListener(this);
addToRosterButton.removeActionListener(this);
}
public void sendMessage() {
String text = getChatInputEditor().getText();
sendMessage(text);
}
public void sendMessage(String text) {
final Message message = new Message();
if (threadID == null) {
threadID = StringUtils.randomString(6);
}
message.setThread(threadID);
// Set the body of the message using typedMessage and remove control
// characters
text = text.replaceAll("[\\u0001-\\u0008\\u000B-\\u001F]", "");
message.setBody(text);
// IF there is no body, just return and do nothing
if (!ModelUtil.hasLength(text)) {
return;
}
// Fire Message Filters
SparkManager.getChatManager().filterOutgoingMessage(this, message);
// Fire Global Filters
SparkManager.getChatManager().fireGlobalMessageSentListeners(this, message);
sendMessage(message);
}
/**
* Sends a message to the appropriate jid. The message is automatically added to the transcript.
*
* @param message the message to send.
*/
public void sendMessage(Message message) {
lastActivity = System.currentTimeMillis();
//Before sending message, let's add our full jid for full verification
//Set message attributes before insertMessage is called - this is useful when transcript window is extended
//more information will be available to be displayed for the chat area Document
message.setType(Message.Type.chat);
message.setTo(participantJID);
message.setFrom(SparkManager.getSessionManager().getJID());
try {
getTranscriptWindow().insertMessage(getNickname(), message, ChatManager.TO_COLOR, TRANSPARENT_COLOR);
getChatInputEditor().selectAll();
getTranscriptWindow().validate();
getTranscriptWindow().repaint();
getChatInputEditor().clear();
}
catch (Exception ex) {
Log.error("Error sending message", ex);
}
// Notify users that message has been sent
fireMessageSent(message);
addToTranscript(message, false);
getChatInputEditor().setCaretPosition(0);
getChatInputEditor().requestFocusInWindow();
scrollToBottom();
// No need to request displayed or delivered as we aren't doing anything with this
// information.
MessageEventManager.addNotificationsRequests(message, true, false, false, true);
// Send the message that contains the notifications request
try {
fireOutgoingMessageSending(message);
SparkManager.getConnection().sendPacket(message);
}
catch (Exception ex) {
Log.error("Error sending message", ex);
}
sendChatStateNotification = true;
activateChatStateNotificationSystem();
}
public String getRoomname() {
return roomname;
}
public Icon getTabIcon() {
return tabIcon;
}
public void setTabIcon(Icon icon) {
this.tabIcon = icon;
}
public String getTabTitle() {
return tabTitle;
}
public void setTabTitle(String tabTitle) {
this.tabTitle = tabTitle;
}
public void setRoomTitle(String roomTitle) {
this.roomTitle = roomTitle;
}
public String getRoomTitle() {
return roomTitle;
}
public Message.Type getChatType() {
return Message.Type.chat;
}
public void leaveChatRoom() {
// There really is no such thing in Agent to Agent
}
public boolean isActive() {
return true;
}
/**
* Returns the Bare-Participant JID
*
* <b> user@server.com </b> <br>
* for retrieving the full Jid use ChatRoomImpl.getJID()
*
* @return
*/
public String getParticipantJID() {
return participantJID;
}
/**
* Returns the users full jid (ex. macbeth@jivesoftware.com/spark).
*
* @return the users Full JID.
*/
public String getJID() {
presence = PresenceManager.getPresence(getParticipantJID());
return presence.getFrom();
}
/**
* Process incoming packets.
*
* @param packet - the packet to process
*/
public void processPacket(final Packet packet) {
final Runnable runnable = new Runnable() {
public void run() {
if (packet instanceof Presence) {
presence = (Presence)packet;
final Presence presence = (Presence)packet;
ContactList list = SparkManager.getWorkspace().getContactList();
ContactItem contactItem = list.getContactItemByJID(getParticipantJID());
String time = DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date());
if (presence.getType() == Presence.Type.unavailable && contactItem != null) {
if (!isOnline()) {
getTranscriptWindow().insertNotificationMessage("*** " + Res.getString("message.went.offline", participantNickname, time), ChatManager.NOTIFICATION_COLOR);
}
}
else if (presence.getType() == Presence.Type.available) {
if (!isOnline()) {
getTranscriptWindow().insertNotificationMessage("*** " + Res.getString("message.came.online", participantNickname, time), ChatManager.NOTIFICATION_COLOR);
}
}
}
else if (packet instanceof Message) {
lastActivity = System.currentTimeMillis();
// Do something with the incoming packet here.
final Message message = (Message)packet;
fireReceivingIncomingMessage(message);
if (message.getError() != null) {
if (message.getError().getCode() == 404) {
// Check to see if the user is online to recieve this message.
RosterEntry entry = roster.getEntry(participantJID);
if (!presence.isAvailable() && !offlineSent && entry != null) {
getTranscriptWindow().insertNotificationMessage(Res.getString("message.offline.error"), ChatManager.ERROR_COLOR);
offlineSent = true;
}
}
return;
}
// Check to see if the user is online to recieve this message.
RosterEntry entry = roster.getEntry(participantJID);
if (!presence.isAvailable() && !offlineSent && entry != null) {
getTranscriptWindow().insertNotificationMessage(Res.getString("message.offline"), ChatManager.ERROR_COLOR);
offlineSent = true;
}
if (threadID == null) {
threadID = message.getThread();
if (threadID == null) {
threadID = StringUtils.randomString(6);
}
}
boolean broadcast = message.getProperty("broadcast") != null;
// If this is a group chat message, discard
if (message.getType() == Message.Type.groupchat || broadcast || message.getType() == Message.Type.normal ||
message.getType() == Message.Type.headline) {
return;
}
// Do not accept Administrative messages.
final String host = SparkManager.getSessionManager().getServerAddress();
if (host.equals(message.getFrom())) {
return;
}
// If the message is not from the current agent. Append to chat.
if (message.getBody() != null) {
participantJID = message.getFrom();
insertMessage(message);
showTyping(false);
}
}
}
};
SwingUtilities.invokeLater(runnable);
}
/**
* Returns the nickname of the user chatting with.
*
* @return the nickname of the chatting user.
*/
public String getParticipantNickname() {
return participantNickname;
}
/**
* The current SendField has been updated somehow.
*
* @param e - the DocumentEvent to respond to.
*/
public void insertUpdate(DocumentEvent e) {
checkForText(e);
if (!sendChatStateNotification) {
return;
}
startNotificationSendingTime = System.currentTimeMillis();
// If the user pauses for more than two seconds, send out a new notice.
if (sendComposingNotification) {
try {
//SparkManager.getMessageEventManager().sendComposingNotification(getParticipantJID(), threadID);
sendChatState(ChatState.composing);
sendPausingNotification = true;
sendInactiveNotification = true;
sendGoneNotification = true;
sendComposingNotification = false;
}
catch (Exception exception) {
Log.error("Error updating", exception);
}
}
}
public void insertMessage(Message message) {
// Debug info
super.insertMessage(message);
MessageEvent messageEvent = (MessageEvent)message.getExtension("x", "jabber:x:event");
if (messageEvent != null) {
checkEvents(message.getFrom(), message.getPacketID(), messageEvent);
}
getTranscriptWindow().insertMessage(participantNickname, message, ChatManager.FROM_COLOR, TRANSPARENT_COLOR);
// Set the participant jid to their full JID.
participantJID = message.getFrom();
}
private void checkEvents(String from, String packetID, MessageEvent messageEvent) {
if (messageEvent.isDelivered() || messageEvent.isDisplayed()) {
// Create the message to send
Message msg = new Message(from);
// Create a MessageEvent Package and add it to the message
MessageEvent event = new MessageEvent();
if (messageEvent.isDelivered()) {
event.setDelivered(true);
}
if (messageEvent.isDisplayed()) {
event.setDisplayed(true);
}
event.setPacketID(packetID);
msg.addExtension(event);
// Send the packet
SparkManager.getConnection().sendPacket(msg);
}
}
public void addMessageEventListener(MessageEventListener listener) {
messageEventListeners.add(listener);
}
public void removeMessageEventListener(MessageEventListener listener) {
messageEventListeners.remove(listener);
}
public Collection<MessageEventListener> getMessageEventListeners() {
return messageEventListeners;
}
public void fireOutgoingMessageSending(Message message) {
for (MessageEventListener messageEventListener : new ArrayList<MessageEventListener>(messageEventListeners)) {
messageEventListener.sendingMessage(message);
}
}
public void fireReceivingIncomingMessage(Message message) {
for (MessageEventListener messageEventListener : new ArrayList<MessageEventListener>(messageEventListeners)) {
messageEventListener.receivingMessage(message);
}
}
/**
* Show the typing notification.
*
* @param typing true if the typing notification should show, otherwise hide it.
*/
public void showTyping(boolean typing) {
if (typing) {
String isTypingText = Res.getString("message.is.typing.a.message", participantNickname);
getNotificationLabel().setText(isTypingText);
getNotificationLabel().setIcon(SparkRes.getImageIcon(SparkRes.SMALL_MESSAGE_EDIT_IMAGE));
}
else {
// Remove is typing text.
getNotificationLabel().setText("");
getNotificationLabel().setIcon(SparkRes.getImageIcon(SparkRes.BLANK_IMAGE));
}
}
/**
* The last time this chat room sent or received a message.
*
* @return the last time this chat room sent or receieved a message.
*/
public long getLastActivity() {
return lastActivity;
}
/**
* Returns the current presence of the client this room was created for.
*
* @return the presence
*/
public Presence getPresence() {
return presence;
}
public void setSendChatStateNotification(boolean isSendChatStateNotification) {
this.sendChatStateNotification = isSendChatStateNotification;
}
public void connectionClosed() {
handleDisconnect();
String message = Res.getString("message.disconnected.error");
getTranscriptWindow().insertNotificationMessage(message, ChatManager.ERROR_COLOR);
}
public void connectionClosedOnError(Exception ex) {
handleDisconnect();
String message = Res.getString("message.disconnected.error");
if (ex instanceof XMPPException) {
XMPPException xmppEx = (XMPPException)ex;
StreamError error = xmppEx.getStreamError();
String reason = error.getCode();
if ("conflict".equals(reason)) {
message = Res.getString("message.disconnected.conflict.error");
}
}
getTranscriptWindow().insertNotificationMessage(message, ChatManager.ERROR_COLOR);
}
public void reconnectionSuccessful() {
Presence usersPresence = PresenceManager.getPresence(getParticipantJID());
if (usersPresence.isAvailable()) {
presence = usersPresence;
}
SparkManager.getChatManager().getChatContainer().fireChatRoomStateUpdated(this);
getChatInputEditor().setEnabled(true);
getSendButton().setEnabled(true);
}
private void handleDisconnect() {
presence = new Presence(Presence.Type.unavailable);
getChatInputEditor().setEnabled(false);
getSendButton().setEnabled(false);
SparkManager.getChatManager().getChatContainer().fireChatRoomStateUpdated(this);
}
protected void loadHistory() {
// Add VCard Panel
vcardPanel = new VCardPanel(participantJID);
getToolBar().add(vcardPanel, new GridBagConstraints(0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.HORIZONTAL, new Insets(0, 2, 0, 2), 0, 0));
final LocalPreferences localPreferences = SettingsManager.getLocalPreferences();
if (!localPreferences.isChatHistoryEnabled()) {
return;
}
if (!localPreferences.isPrevChatHistoryEnabled()) {
return;
}
final ChatTranscript chatTranscript = ChatTranscripts.getCurrentChatTranscript(getParticipantJID());
final String personalNickname = SparkManager.getUserManager().getNickname();
for (HistoryMessage message : chatTranscript.getMessages()) {
String nickname = SparkManager.getUserManager().getUserNicknameFromJID(message.getFrom());
String messageBody = message.getBody();
if (nickname.equals(message.getFrom())) {
String otherJID = StringUtils.parseBareAddress(message.getFrom());
String myJID = SparkManager.getSessionManager().getBareAddress();
if (otherJID.equals(myJID)) {
nickname = personalNickname;
}
else {
try
{
nickname = message.getFrom().substring(message.getFrom().indexOf("/")+1);
}
catch(Exception e)
{
nickname = StringUtils.parseName(nickname);
}
}
}
if (ModelUtil.hasLength(messageBody) && messageBody.startsWith("/me ")) {
messageBody = messageBody.replaceFirst("/me", nickname);
}
final Date messageDate = message.getDate();
getTranscriptWindow().insertHistoryMessage(nickname, messageBody, messageDate);
}
if ( 0 < chatTranscript.getMessages().size() ) { // Check if we have history mesages
getTranscriptWindow().insertHorizontalLine();
}
chatTranscript.release();
}
private boolean isOnline() {
Presence presence = roster.getPresence(getParticipantJID());
return presence.isAvailable();
}
// I would normally use the command pattern, but
// have no real use when dealing with just a couple options.
public void actionPerformed(ActionEvent e) {
if (e.getSource() == infoButton) {
VCardManager vcard = SparkManager.getVCardManager();
vcard.viewProfile(participantJID, SparkManager.getChatManager().getChatContainer());
}
else if (e.getSource() == addToRosterButton) {
RosterDialog rosterDialog = new RosterDialog();
rosterDialog.setDefaultJID(StringUtils.parseBareAddress(participantJID));
rosterDialog.setDefaultNickname(getParticipantNickname());
rosterDialog.showRosterDialog(SparkManager.getChatManager().getChatContainer().getChatFrame());
} else {
super.actionPerformed(e);
}
}
protected TimerTask getTypingTimerTask() {
return typingTimerTask;
}
public void notifyChatStateChange(ChatState state) {
if (chatStatePanel != null) {
getEditorWrapperBar().remove(chatStatePanel);
}
chatStatePanel = new ChatStatePanel(state, getParticipantNickname());
getEditorWrapperBar().add(chatStatePanel, BorderLayout.SOUTH);
getEditorWrapperBar().revalidate();
getEditorWrapperBar().repaint();
}
private class ChatStateFocusListener extends FocusAdapter {
@Override
public void focusGained(FocusEvent e) {
if(e.getComponent().equals(getChatInputEditor())) {
if (sendChatStateNotification) {
activateChatStateNotificationSystem();
}
}
}
@Override
public void focusLost(FocusEvent e) {
if(e.getComponent().equals(getChatInputEditor())) {
inactivateChatStateNotificationSystem();
}
}
}
}