net.java.sip.communicator.impl.gui.main.chat.ChatWritePanel.java Source code

Java tutorial

Introduction

Here is the source code for net.java.sip.communicator.impl.gui.main.chat.ChatWritePanel.java

Source

/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * 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 net.java.sip.communicator.impl.gui.main.chat;

import java.awt.*;
import java.awt.Container;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;

import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.undo.*;

import net.java.sip.communicator.impl.gui.*;
import net.java.sip.communicator.impl.gui.event.*;
import net.java.sip.communicator.impl.gui.main.chat.conference.*;
import net.java.sip.communicator.impl.gui.main.chat.menus.*;
import net.java.sip.communicator.impl.gui.utils.*;
import net.java.sip.communicator.plugin.desktoputil.*;
import net.java.sip.communicator.service.contactlist.*;
import net.java.sip.communicator.service.contactsource.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.gui.event.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.resources.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.skin.*;

import org.apache.commons.lang3.*;
import org.jitsi.service.configuration.*;
import org.osgi.framework.*;

/**
 * The <tt>ChatWritePanel</tt> is the panel, where user writes her messages.
 * It is located at the bottom of the split in the <tt>ChatPanel</tt> and it
 * contains an editor, where user writes the text.
 *
 * @author Yana Stamcheva
 * @author Lyubomir Marinov
 * @author Adam Netocny
 */
public class ChatWritePanel extends TransparentPanel implements ActionListener, KeyListener, MouseListener,
        UndoableEditListener, DocumentListener, PluginComponentListener, Skinnable, ChatSessionChangeListener {
    /**
     * The <tt>Logger</tt> used by the <tt>ChatWritePanel</tt> class and its
     * instances for logging output.
     */
    private static final Logger logger = Logger.getLogger(ChatWritePanel.class);

    private final JEditorPane editorPane = new JEditorPane();

    private final UndoManager undo = new UndoManager();

    private final ChatPanel chatPanel;

    private final Timer stoppedTypingTimer = new Timer(2 * 1000, this);

    private final Timer typingTimer = new Timer(5 * 1000, this);

    private int typingState = OperationSetTypingNotifications.STATE_STOPPED;

    private WritePanelRightButtonMenu rightButtonMenu;

    private final ArrayList<ChatMenuListener> menuListeners = new ArrayList<ChatMenuListener>();

    private final SIPCommScrollPane scrollPane = new SIPCommScrollPane();

    private ChatTransportSelectorBox transportSelectorBox;

    private final Container centerPanel;

    private SIPCommToggleButton smsButton;

    private JLabel smsCharCountLabel;

    private JLabel smsNumberLabel;

    private int smsNumberCount = 1;

    private int smsCharCount = 160;

    private boolean smsMode = false;

    /**
     * Mode where we do not mix sending im and sms in one chat window.
     */
    private boolean disableMergedSmsMode = false;

    /**
     * Property to control merge sms mode.
     */
    private static final String MERGE_SMS_MODE_DISABLED_PROP = "net.java.sip.communicator.impl.gui.MERGE_SMS_MODE_DISABLED";

    /**
     * A timer used to reset the transport resource to the bare ID if there was
     * no activity from this resource since a bunch of time.
     */
    private java.util.Timer outdatedResourceTimer = null;

    /**
     * Tells if the current resource is outdated. A timer has already been
     * triggered, but when there is only a single resource there is no bare ID
     * available. Thus, flag this resource as outdated to switch to the bare ID
     * when available.
     */
    private boolean isOutdatedResource = true;

    /**
     * List of plugin components that are registered for updates.
     */
    private List<PluginComponent> pluginComponents = Collections.synchronizedList(new ArrayList<PluginComponent>());

    /**
     * Creates an instance of <tt>ChatWritePanel</tt>.
     *
     * @param panel The parent <tt>ChatPanel</tt>.
     */
    public ChatWritePanel(ChatPanel panel) {
        super(new BorderLayout());

        this.chatPanel = panel;

        centerPanel = createCenter();

        int chatAreaSize = ConfigurationUtils.getChatWriteAreaSize();
        Dimension writeMessagePanelDefaultSize = new Dimension(500, (chatAreaSize > 0) ? chatAreaSize : 28);
        Dimension writeMessagePanelMinSize = new Dimension(500, 28);
        Dimension writeMessagePanelMaxSize = new Dimension(500, 100);

        setMinimumSize(writeMessagePanelMinSize);
        setMaximumSize(writeMessagePanelMaxSize);
        setPreferredSize(writeMessagePanelDefaultSize);

        this.add(centerPanel, BorderLayout.CENTER);

        this.rightButtonMenu = new WritePanelRightButtonMenu(chatPanel.getChatContainer());

        this.typingTimer.setRepeats(true);

        // initialize send command to Ctrl+Enter
        ConfigurationService configService = GuiActivator.getConfigurationService();

        disableMergedSmsMode = configService.getBoolean(MERGE_SMS_MODE_DISABLED_PROP, disableMergedSmsMode);

        String messageCommandProperty = "service.gui.SEND_MESSAGE_COMMAND";
        String messageCommand = configService.getString(messageCommandProperty);

        if (messageCommand == null)
            messageCommand = GuiActivator.getResources().getSettingsString(messageCommandProperty);

        this.changeSendCommand((messageCommand == null || messageCommand.equalsIgnoreCase("enter")));

        if (ConfigurationUtils.isFontSupportEnabled())
            initDefaultFontConfiguration();
    }

    /**
     * Initializes the default font configuration for this chat write area.
     */
    private void initDefaultFontConfiguration() {
        String fontFamily = ConfigurationUtils.getChatDefaultFontFamily();
        int fontSize = ConfigurationUtils.getChatDefaultFontSize();

        // Font family and size
        if (fontFamily != null && fontSize > 0)
            setFontFamilyAndSize(fontFamily, fontSize);

        // Font style
        setBoldStyleEnable(ConfigurationUtils.isChatFontBold());
        setItalicStyleEnable(ConfigurationUtils.isChatFontItalic());
        setUnderlineStyleEnable(ConfigurationUtils.isChatFontUnderline());

        // Font color
        Color fontColor = ConfigurationUtils.getChatDefaultFontColor();

        if (fontColor != null)
            setFontColor(fontColor);
    }

    /**
     * Creates the center panel.
     *
     * @return the created center panel
     */
    private Container createCenter() {
        JPanel centerPanel = new JPanel(new GridBagLayout());

        centerPanel.setBackground(Color.WHITE);
        centerPanel.setBorder(BorderFactory.createEmptyBorder(3, 0, 3, 3));

        GridBagConstraints constraints = new GridBagConstraints();

        initSmsLabel(centerPanel);
        initTextArea(centerPanel);

        smsCharCountLabel = new JLabel(String.valueOf(smsCharCount));
        smsCharCountLabel.setForeground(Color.GRAY);
        smsCharCountLabel.setVisible(false);

        constraints.anchor = GridBagConstraints.NORTHEAST;
        constraints.fill = GridBagConstraints.NONE;
        constraints.gridx = 3;
        constraints.gridy = 0;
        constraints.weightx = 0f;
        constraints.weighty = 0f;
        constraints.insets = new Insets(0, 2, 0, 2);
        constraints.gridheight = 1;
        constraints.gridwidth = 1;
        centerPanel.add(smsCharCountLabel, constraints);

        smsNumberLabel = new JLabel(String.valueOf(smsNumberCount)) {
            @Override
            public void paintComponent(Graphics g) {
                AntialiasingManager.activateAntialiasing(g);
                g.setColor(getBackground());
                g.fillOval(0, 0, getWidth(), getHeight());

                super.paintComponent(g);
            }
        };
        smsNumberLabel.setHorizontalAlignment(JLabel.CENTER);
        smsNumberLabel.setPreferredSize(new Dimension(18, 18));
        smsNumberLabel.setMinimumSize(new Dimension(18, 18));
        smsNumberLabel.setForeground(Color.WHITE);
        smsNumberLabel.setBackground(Color.GRAY);
        smsNumberLabel.setVisible(false);

        constraints.anchor = GridBagConstraints.NORTHEAST;
        constraints.fill = GridBagConstraints.NONE;
        constraints.gridx = 4;
        constraints.gridy = 0;
        constraints.weightx = 0f;
        constraints.weighty = 0f;
        constraints.insets = new Insets(0, 2, 0, 2);
        constraints.gridheight = 1;
        constraints.gridwidth = 1;
        centerPanel.add(smsNumberLabel, constraints);

        return centerPanel;
    }

    /**
     * Initializes the sms menu.
     *
     * @param centerPanel the parent panel
     */
    private void initSmsLabel(final JPanel centerPanel) {
        GridBagConstraints constraints = new GridBagConstraints();

        constraints.anchor = GridBagConstraints.NORTHWEST;
        constraints.fill = GridBagConstraints.NONE;
        constraints.gridx = 1;
        constraints.gridy = 0;
        constraints.gridheight = 1;
        constraints.weightx = 0f;
        constraints.weighty = 0f;
        constraints.insets = new Insets(0, 3, 0, 0);

        ImageID smsIcon = new ImageID("service.gui.icons.SEND_SMS");

        ImageID selectedIcon = new ImageID("service.gui.icons.SEND_SMS_SELECTED");

        smsButton = new SIPCommToggleButton(ImageLoader.getImage(smsIcon), ImageLoader.getImage(selectedIcon),
                ImageLoader.getImage(smsIcon), ImageLoader.getImage(selectedIcon));

        smsButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (smsMode && !isIMAllowed()) {
                    return;
                }

                smsMode = smsButton.isSelected();

                Color bgColor;
                if (smsMode) {
                    bgColor = new Color(GuiActivator.getResources().getColor("service.gui.LIST_SELECTION_COLOR"));

                    smsCharCountLabel.setVisible(true);
                    smsNumberLabel.setVisible(true);
                } else {
                    bgColor = Color.WHITE;
                    smsCharCountLabel.setVisible(false);
                    smsNumberLabel.setVisible(false);
                }

                centerPanel.setBackground(bgColor);
                editorPane.setBackground(bgColor);
            }
        });

        // We hide the sms label until we know if the chat supports sms.
        smsButton.setVisible(false);

        centerPanel.add(smsButton, constraints);
    }

    private void initTextArea(JPanel centerPanel) {
        GridBagConstraints constraints = new GridBagConstraints();

        editorPane.setContentType("text/html");
        editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
        editorPane.setCaretPosition(0);
        editorPane.setEditorKit(new SIPCommHTMLEditorKit(this));
        editorPane.getDocument().addUndoableEditListener(this);
        editorPane.getDocument().addDocumentListener(this);
        editorPane.addKeyListener(this);
        editorPane.addMouseListener(this);
        editorPane.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
        editorPane.setDragEnabled(true);
        editorPane.setTransferHandler(new ChatTransferHandler(chatPanel));

        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);

        scrollPane.setOpaque(false);
        scrollPane.getViewport().setOpaque(false);
        scrollPane.setBorder(null);

        scrollPane.setViewportView(editorPane);

        constraints.anchor = GridBagConstraints.NORTHWEST;
        constraints.fill = GridBagConstraints.BOTH;
        constraints.gridx = 2;
        constraints.gridy = 0;
        constraints.weightx = 1f;
        constraints.weighty = 1f;
        constraints.gridheight = 1;
        constraints.gridwidth = 1;
        constraints.insets = new Insets(0, 0, 0, 0);
        centerPanel.add(scrollPane, constraints);
    }

    /**
     * Runs clean-up for associated resources which need explicit disposal (e.g.
     * listeners keeping this instance alive because they were added to the
     * model which operationally outlives this instance).
     */
    public void dispose() {
        /*
         * Stop the Timers because they're implicitly globally referenced and
         * thus don't let them retain this instance.
         */
        typingTimer.stop();
        typingTimer.removeActionListener(this);
        stoppedTypingTimer.stop();
        stoppedTypingTimer.removeActionListener(this);
        if (typingState != OperationSetTypingNotifications.STATE_STOPPED)
            stopTypingTimer();

        if (outdatedResourceTimer != null) {
            outdatedResourceTimer.cancel();
            outdatedResourceTimer.purge();
            outdatedResourceTimer = null;
        }

        editorPane.removeKeyListener(this);
        menuListeners.clear();

        if (rightButtonMenu != null) {
            rightButtonMenu.dispose();
            rightButtonMenu = null;
        }

        scrollPane.dispose();
    }

    /**
     * Returns the editor panel, contained in this <tt>ChatWritePanel</tt>.
     *
     * @return The editor panel, contained in this <tt>ChatWritePanel</tt>.
     */
    public JEditorPane getEditorPane() {
        return editorPane;
    }

    /**
     * Replaces the Ctrl+Enter send command with simple Enter.
     *
     * @param isEnter indicates if the new send command is enter or cmd-enter
     */
    public void changeSendCommand(boolean isEnter) {
        ActionMap actionMap = editorPane.getActionMap();
        actionMap.put("send", new SendMessageAction());
        actionMap.put("newLine", new NewLineAction());

        InputMap im = this.editorPane.getInputMap();

        if (isEnter) {
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "send");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), "newLine");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK), "newLine");

            this.setToolTipText("<html>" + GuiActivator.getResources().getI18NString("service.gui.SEND_MESSAGE")
                    + " - Enter <br> " + "Use Ctrl-Enter or Shift-Enter to make a new line" + "</html>");
        } else {
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), "send");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "newLine");

            this.setToolTipText(
                    GuiActivator.getResources().getI18NString("service.gui.SEND_MESSAGE") + " Ctrl-Enter");
        }
    }

    /**
     * Enables/disables the sms mode.
     *
     * @param selected <tt>true</tt> to enable sms mode, <tt>false</tt> -
     * otherwise
     */
    public void setSmsSelected(boolean selected) {
        if (disableMergedSmsMode && isIMAllowed())
            return;

        if ((selected && !smsButton.isSelected()) || (!selected && smsButton.isSelected())) {
            smsButton.doClick();
        }
    }

    /**
     * Returns <tt>true</tt> if the sms mode is enabled, otherwise returns
     * <tt>false</tt>.
     * @return <tt>true</tt> if the sms mode is enabled, otherwise returns
     * <tt>false</tt>
     */
    public boolean isSmsSelected() {
        return smsMode;
    }

    /**
     * Checks if sending IM message is allowed. When in sms mode, it
     * can be the only method to send message. We will disable sms -> im
     * switching.
     * @return is IM allowed.
     */
    private boolean isIMAllowed() {
        // check are we allowed to change back to im mode
        Object descr = chatPanel.getChatSession().getDescriptor();

        if (descr instanceof MetaContact) {
            List<Contact> imContact = ((MetaContact) descr)
                    .getContactsForOperationSet(OperationSetBasicInstantMessaging.class);

            if (imContact == null || imContact.size() == 0)
                return false;
        } else if (descr instanceof SourceContact) {
            List<ContactDetail> imContact = ((SourceContact) descr)
                    .getContactDetails(OperationSetBasicInstantMessaging.class);

            if (imContact == null || imContact.size() == 0)
                return false;
        }

        return true;
    }

    /**
     * The <tt>SendMessageAction</tt> is an <tt>AbstractAction</tt> that
     * sends the text that is currently in the write message area.
     */
    private class SendMessageAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            // chatPanel.stopTypingNotifications();
            chatPanel.sendButtonDoClick();
        }
    }

    /**
     * The <tt>NewLineAction</tt> is an <tt>AbstractAction</tt> that types
     * an enter in the write message area.
     */
    private class NewLineAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            int caretPosition = editorPane.getCaretPosition();
            HTMLDocument doc = (HTMLDocument) editorPane.getDocument();

            try {
                doc.insertString(caretPosition, "\n", null);
            } catch (BadLocationException e1) {
                logger.error("Could not insert <br> to the document.", e1);
            }

            editorPane.setCaretPosition(caretPosition + 1);
        }
    }

    /**
     * Handles the <tt>UndoableEditEvent</tt>, by adding the content edit to
     * the <tt>UndoManager</tt>.
     *
     * @param e The <tt>UndoableEditEvent</tt>.
     */
    public void undoableEditHappened(UndoableEditEvent e) {
        this.undo.addEdit(e.getEdit());
    }

    /**
     * Implements the undo operation.
     */
    private void undo() {
        try {
            undo.undo();
        } catch (CannotUndoException e) {
            logger.error("Unable to undo.", e);
        }
    }

    /**
     * Implements the redo operation.
     */
    private void redo() {
        try {
            undo.redo();
        } catch (CannotRedoException e) {
            logger.error("Unable to redo.", e);
        }
    }

    /**
     * Sends typing notifications when user types.
     *
     * @param e the event.
     */
    public void keyTyped(KeyEvent e) {
        if (ConfigurationUtils.isSendTypingNotifications() && !smsMode) {
            if (typingState != OperationSetTypingNotifications.STATE_TYPING) {
                stoppedTypingTimer.setDelay(2 * 1000);
                typingState = OperationSetTypingNotifications.STATE_TYPING;

                int result = chatPanel.getChatSession().getCurrentChatTransport()
                        .sendTypingNotification(typingState);

                if (result == ChatPanel.TYPING_NOTIFICATION_SUCCESSFULLY_SENT)
                    typingTimer.start();
            }

            if (!stoppedTypingTimer.isRunning())
                stoppedTypingTimer.start();
            else
                stoppedTypingTimer.restart();
        }
    }

    /**
     * When CTRL+Z is pressed invokes the <code>ChatWritePanel.undo()</code>
     * method, when CTRL+R is pressed invokes the
     * <code>ChatWritePanel.redo()</code> method.
     *
     * @param e the <tt>KeyEvent</tt> that notified us
     */
    public void keyPressed(KeyEvent e) {
        if ((e.getModifiers() & KeyEvent.CTRL_MASK) == KeyEvent.CTRL_MASK && (e.getKeyCode() == KeyEvent.VK_Z)
        // And not ALT(right ALT gives CTRL + ALT).
                && (e.getModifiers() & KeyEvent.ALT_MASK) != KeyEvent.ALT_MASK) {
            if (undo.canUndo())
                undo();
        } else if ((e.getModifiers() & KeyEvent.CTRL_MASK) == KeyEvent.CTRL_MASK
                && (e.getKeyCode() == KeyEvent.VK_R)
                // And not ALT(right ALT gives CTRL + ALT).
                && (e.getModifiers() & KeyEvent.ALT_MASK) != KeyEvent.ALT_MASK) {
            if (undo.canRedo())
                redo();
        } else if (e.getKeyCode() == KeyEvent.VK_TAB) {
            if (!(chatPanel.getChatSession() instanceof ConferenceChatSession))
                return;

            e.consume();
            int index = ((JEditorPane) e.getSource()).getCaretPosition();

            StringBuffer message = new StringBuffer(chatPanel.getMessage());

            int position = index - 1;

            while (position > 0 && (message.charAt(position) != ' ')) {
                position--;
            }

            if (position != 0)
                position++;

            String sequence = message.substring(position, index);

            if (sequence.length() <= 0) {
                // Do not look for matching contacts if the matching pattern is
                // 0 chars long, since all contacts will match.
                return;
            }

            Iterator<ChatContact<?>> iter = chatPanel.getChatSession().getParticipants();
            ArrayList<String> contacts = new ArrayList<String>();
            while (iter.hasNext()) {
                ChatContact<?> c = iter.next();
                if (c.getName().length() >= (index - position)
                        && c.getName().substring(0, index - position).equals(sequence)) {
                    message.replace(position, index, c.getName().substring(0, index - position));
                    contacts.add(c.getName());
                }
            }

            if (contacts.size() > 1) {
                char key = contacts.get(0).charAt(index - position - 1);
                int pos = index - position - 1;
                boolean flag = true;

                while (flag) {
                    try {
                        for (String name : contacts) {
                            if (key != name.charAt(pos)) {
                                flag = false;
                            }
                        }

                        if (flag) {
                            pos++;
                            key = contacts.get(0).charAt(pos);
                        }
                    } catch (IndexOutOfBoundsException exp) {
                        flag = false;
                    }
                }

                message.replace(position, index, contacts.get(0).substring(0, pos));

                Iterator<String> contactIter = contacts.iterator();
                String contactList = "<DIV align='left'><h5>";
                while (contactIter.hasNext()) {
                    contactList += contactIter.next() + " ";
                }
                contactList += "</h5></DIV>";

                chatPanel.getChatConversationPanel().appendMessageToEnd(contactList,
                        ChatHtmlUtils.HTML_CONTENT_TYPE);
            } else if (contacts.size() == 1) {
                String limiter = (position == 0) ? ": " : "";
                message.replace(position, index, contacts.get(0) + limiter);
            }

            try {
                ((JEditorPane) e.getSource()).getDocument().remove(0,
                        ((JEditorPane) e.getSource()).getDocument().getLength());
                ((JEditorPane) e.getSource()).getDocument().insertString(0, message.toString(), null);
            } catch (BadLocationException ex) {
                ex.printStackTrace();
            }
        } else if (e.getKeyCode() == KeyEvent.VK_UP) {
            // Only enters editing mode if the write panel is empty in
            // order not to lose the current message contents, if any.
            if (this.chatPanel.getLastSentMessageUID() != null && this.chatPanel.isWriteAreaEmpty()) {
                this.chatPanel.startLastMessageCorrection();
                e.consume();
            }
        } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
            if (chatPanel.isMessageCorrectionActive()) {
                Document doc = editorPane.getDocument();
                if (editorPane.getCaretPosition() == doc.getLength()) {
                    chatPanel.stopMessageCorrection();
                }
            }
        }
    }

    public void keyReleased(KeyEvent e) {
    }

    /**
     * Performs actions when typing timer has expired.
     *
     * @param e the <tt>ActionEvent</tt> that notified us
     */
    public void actionPerformed(ActionEvent e) {
        Object source = e.getSource();

        if (typingTimer.equals(source)) {
            if (typingState == OperationSetTypingNotifications.STATE_TYPING) {
                chatPanel.getChatSession().getCurrentChatTransport()
                        .sendTypingNotification(OperationSetTypingNotifications.STATE_TYPING);
            }
        } else if (stoppedTypingTimer.equals(source)) {
            typingTimer.stop();
            if (typingState == OperationSetTypingNotifications.STATE_TYPING) {
                try {
                    typingState = OperationSetTypingNotifications.STATE_PAUSED;

                    int result = chatPanel.getChatSession().getCurrentChatTransport()
                            .sendTypingNotification(typingState);

                    if (result == ChatPanel.TYPING_NOTIFICATION_SUCCESSFULLY_SENT)
                        stoppedTypingTimer.setDelay(3 * 1000);
                } catch (Exception ex) {
                    logger.error("Failed to send typing notifications.", ex);
                }
            } else if (typingState == OperationSetTypingNotifications.STATE_PAUSED) {
                stopTypingTimer();
            }
        }
    }

    /**
     * Stops the timer and sends a notification message.
     */
    public void stopTypingTimer() {
        typingState = OperationSetTypingNotifications.STATE_STOPPED;

        int result = chatPanel.getChatSession().getCurrentChatTransport().sendTypingNotification(typingState);

        if (result == ChatPanel.TYPING_NOTIFICATION_SUCCESSFULLY_SENT)
            stoppedTypingTimer.stop();
    }

    /**
     * Opens the <tt>WritePanelRightButtonMenu</tt> when user clicks with the
     * right mouse button on the editor area.
     *
     * @param e the <tt>MouseEvent</tt> that notified us
     */
    public void mouseClicked(MouseEvent e) {
        if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0 || (e.isControlDown() && !e.isMetaDown())) {
            Point p = e.getPoint();
            SwingUtilities.convertPointToScreen(p, e.getComponent());

            //SPELLCHECK
            ArrayList<JMenuItem> contributedMenuEntries = new ArrayList<JMenuItem>();

            for (ChatMenuListener listener : this.menuListeners) {
                contributedMenuEntries.addAll(listener.getMenuElements(this.chatPanel, e));
            }

            for (JMenuItem item : contributedMenuEntries) {
                rightButtonMenu.add(item);
            }

            JPopupMenu rightMenu = rightButtonMenu.makeMenu(contributedMenuEntries);
            rightMenu.setInvoker(editorPane);
            rightMenu.setLocation(p.x, p.y);
            rightMenu.setVisible(true);
        }
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseReleased(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    /**
     * Returns the <tt>WritePanelRightButtonMenu</tt> opened in this panel.
     * Used by the <tt>ChatWindow</tt>, when the ESC button is pressed, to
     * check if there is an open menu, which should be closed.
     *
     * @return the <tt>WritePanelRightButtonMenu</tt> opened in this panel
     */
    public WritePanelRightButtonMenu getRightButtonMenu() {
        return rightButtonMenu;
    }

    /**
     * Returns the write area text as an html text.
     *
     * @return the write area text as an html text.
     */
    public String getTextAsHtml() {
        String msgText = editorPane.getText();

        String formattedString = msgText.replaceAll("<html>|<head>|<body>|</html>|</head>|</body>", "");

        formattedString = extractFormattedText(formattedString);
        // Returned string is formatted with newlines, etc. so we need to get
        // rid of them before checking for the ending <br/>.
        formattedString = formattedString.trim();

        if (formattedString.endsWith("<BR/>"))
            formattedString = formattedString.substring(0, formattedString.lastIndexOf("<BR/>"));

        return formattedString;
    }

    /**
     * Returns the write area text as a plain text without any formatting.
     *
     * @return the write area text as a plain text without any formatting.
     */
    public String getText() {
        try {
            Document doc = editorPane.getDocument();

            return doc.getText(0, doc.getLength());
        } catch (BadLocationException e) {
            logger.error("Could not obtain write area text.", e);
        }

        return null;
    }

    /**
     * Clears write message area.
     */
    public void clearWriteArea() {
        try {
            this.editorPane.getDocument().remove(0, editorPane.getDocument().getLength());

            if (smsMode) {
                // use this to reset sms counter
                setSmsLabelVisible(true);
                // set the reset values
                smsCharCountLabel.setText(String.valueOf(smsCharCount));
                smsNumberLabel.setText(String.valueOf(smsNumberCount));
            }
        } catch (BadLocationException e) {
            logger.error("Failed to obtain write panel document content.", e);
        }
    }

    /**
     * Appends the given text to the end of the contained HTML document. This
     * method is used to insert smileys when user selects a smiley from the
     * menu.
     *
     * @param text the text to append.
     */
    public void appendText(String text) {
        HTMLDocument doc = (HTMLDocument) editorPane.getDocument();

        Element currentElement = doc.getCharacterElement(editorPane.getCaretPosition());

        try {
            doc.insertAfterEnd(currentElement, text);
        } catch (BadLocationException e) {
            logger.error("Insert in the HTMLDocument failed.", e);
        } catch (IOException e) {
            logger.error("Insert in the HTMLDocument failed.", e);
        }

        this.editorPane.setCaretPosition(doc.getLength());
    }

    /**
     * Return all html paragraph content separated by <BR/> tags.
     *
     * @param msgText the html text.
     * @return the string containing only paragraph content.
     */
    private String extractFormattedText(String msgText) {
        String resultString = msgText.replaceAll("<p\\b[^>]*>", "");

        return resultString.replaceAll("<\\/p>", "<BR/>");
    }

    /**
     * Initializes the send via label and selector box.
     *
     * @return the chat transport selector box
     */
    private Component createChatTransportSelectorBox() {
        // Initialize the "send via" selector box and adds it to the send panel.
        if (transportSelectorBox == null) {
            transportSelectorBox = new ChatTransportSelectorBox(chatPanel, chatPanel.getChatSession(),
                    chatPanel.getChatSession().getCurrentChatTransport());

            if ((ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled()
                    && transportSelectorBox.getMenu().getItemCount() <= 1) || !isIMAllowed())
                transportSelectorBox.setVisible(false);
        }

        return transportSelectorBox;
    }

    /**
     *
     * @param isVisible
     */
    public void setTransportSelectorBoxVisible(boolean isVisible) {
        if (isVisible) {
            if (transportSelectorBox == null) {
                createChatTransportSelectorBox();

                if (!transportSelectorBox.getMenu().isEnabled()) {
                    // Show a message to the user that IM is not possible.
                    chatPanel.getChatConversationPanel()
                            .appendMessageToEnd(
                                    "<h5>" + StringEscapeUtils.escapeHtml4(GuiActivator.getResources()
                                            .getI18NString("service.gui.MSG_NOT_POSSIBLE")) + "</h5>",
                                    ChatHtmlUtils.HTML_CONTENT_TYPE);
                } else {
                    GridBagConstraints constraints = new GridBagConstraints();
                    constraints.anchor = GridBagConstraints.NORTHEAST;
                    constraints.fill = GridBagConstraints.NONE;
                    constraints.gridx = 0;
                    constraints.gridy = 0;
                    constraints.weightx = 0f;
                    constraints.weighty = 0f;
                    constraints.gridheight = 1;
                    constraints.gridwidth = 1;

                    centerPanel.add(transportSelectorBox, constraints, 0);
                }
            } else {
                if (ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled()
                        && transportSelectorBox.getMenu().getItemCount() <= 1) {
                    transportSelectorBox.setVisible(false);
                }
                {
                    transportSelectorBox.setVisible(true);
                }
                centerPanel.repaint();
            }
        } else if (transportSelectorBox != null) {
            transportSelectorBox.setVisible(false);
            centerPanel.repaint();
        }
    }

    /**
     * Selects the given chat transport in the send via box.
     *
     * @param chatTransport The chat transport to be selected.
     * @param isMessageOrFileTransferReceived Boolean telling us if this change
     * of the chat transport correspond to an effective switch to this new
     * transform (a mesaage received from this transport, or a file transfer
     * request received, or if the resource timeouted), or just a status update
     * telling us a new chatTransport is now available (i.e. another device has
     * startup).
     */
    public void setSelectedChatTransport(final ChatTransport chatTransport,
            final boolean isMessageOrFileTransferReceived) {
        // We need to be sure that the following code is executed in the event
        // dispatch thread.
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    setSelectedChatTransport(chatTransport, isMessageOrFileTransferReceived);
                }
            });
            return;
        }

        // Check if this contact provider can manages several resources and thus
        // provides a resource timeout via the basic IM operation set.
        long timeout = -1;
        OperationSetBasicInstantMessaging opSetBasicIM = chatTransport.getProtocolProvider()
                .getOperationSet(OperationSetBasicInstantMessaging.class);
        if (opSetBasicIM != null) {
            timeout = opSetBasicIM.getInactivityTimeout();
        }

        if (isMessageOrFileTransferReceived) {
            isOutdatedResource = false;
        }

        // If this contact supports several resources, then schedule the timer:
        // - If the resource is outdated, then trigger the timer now (to try to
        // switch to the bare ID if now available).
        // - If the new reousrce transport is really effective (i.e. we have
        // received a message from this resource).
        if (timeout != -1 && (isMessageOrFileTransferReceived || isOutdatedResource)) {
            // If there was already a timeout, but the bare ID was not available
            // (i.e. a single resource present). Then call the timeout procedure
            // now in order to switch to the bare ID.
            if (isOutdatedResource) {
                timeout = 0;
            }
            // Cancels the preceding timer.
            if (outdatedResourceTimer != null) {
                outdatedResourceTimer.cancel();
                outdatedResourceTimer.purge();
            }
            // Schedules the timer.
            if (chatTransport.getResourceName() != null) {
                OutdatedResourceTimerTask task = new OutdatedResourceTimerTask();
                outdatedResourceTimer = new java.util.Timer();
                outdatedResourceTimer.schedule(task, timeout);
            }
        }

        // Sets the new resource transport is really effective (i.e. we have
        // received a message from this resource).
        // if we do not have any selected resource, or the currently selected
        // if offline
        if (transportSelectorBox != null
                && (isMessageOrFileTransferReceived || (!transportSelectorBox.hasSelectedTransport()
                        || !chatPanel.getChatSession().getCurrentChatTransport().getStatus().isOnline()))) {
            transportSelectorBox.setSelected(chatTransport);
        }
    }

    /**
     * Adds the given chatTransport to the given send via selector box.
     *
     * @param chatTransport the transport to add
     */
    public void addChatTransport(final ChatTransport chatTransport) {
        // We need to be sure that the following code is executed in the event
        // dispatch thread.
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    addChatTransport(chatTransport);
                }
            });
            return;
        }

        if (transportSelectorBox != null) {
            transportSelectorBox.addChatTransport(chatTransport);

            // it was hidden cause we wanted to hide when there is only one
            // provider
            if (!transportSelectorBox.isVisible() && ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled()
                    && transportSelectorBox.getMenu().getItemCount() > 1) {
                transportSelectorBox.setVisible(true);
            }
        }
    }

    /**
     * Updates the status of the given chat transport in the send via selector
     * box and notifies the user for the status change.
     * @param chatTransport the <tt>chatTransport</tt> to update
     */
    public void updateChatTransportStatus(final ChatTransport chatTransport) {
        // We need to be sure that the following code is executed in the event
        // dispatch thread.
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    updateChatTransportStatus(chatTransport);
                }
            });
            return;
        }

        if (transportSelectorBox != null)
            transportSelectorBox.updateTransportStatus(chatTransport);
    }

    /**
     * Opens the selector box containing the protocol contact icons.
     * This is the menu, where user could select the protocol specific
     * contact to communicate through.
     */
    public void openChatTransportSelectorBox() {
        transportSelectorBox.getMenu().doClick();
    }

    /**
     * Removes the given chat status state from the send via selector box.
     *
     * @param chatTransport the transport to remove
     */
    public void removeChatTransport(final ChatTransport chatTransport) {
        // We need to be sure that the following code is executed in the event
        // dispatch thread.
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    removeChatTransport(chatTransport);
                }
            });
            return;
        }

        if (transportSelectorBox != null)
            transportSelectorBox.removeChatTransport(chatTransport);

        if (transportSelectorBox != null && transportSelectorBox.getMenu().getItemCount() == 1
                && ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled()) {
            transportSelectorBox.setVisible(false);
        }
    }

    /**
     * Show the sms menu.
     * @param isVisible <tt>true</tt> to show the sms menu, <tt>false</tt> -
     * otherwise
     */
    public void setSmsLabelVisible(boolean isVisible) {
        if (disableMergedSmsMode && isIMAllowed())
            return;

        // Re-init sms count properties.
        smsCharCount = 160;
        smsNumberCount = 1;

        smsButton.setVisible(isVisible);

        centerPanel.repaint();
    }

    /**
     * Saves the given font configuration as default, thus making it the default
     * configuration for all chats.
     *
     * @param fontFamily the font family
     * @param fontSize the font size
     * @param isBold indicates if the font is bold
     * @param isItalic indicates if the font is italic
     * @param isUnderline indicates if the font is underline
     */
    public void saveDefaultFontConfiguration(String fontFamily, int fontSize, boolean isBold, boolean isItalic,
            boolean isUnderline, Color color) {
        ConfigurationUtils.setChatDefaultFontFamily(fontFamily);
        ConfigurationUtils.setChatDefaultFontSize(fontSize);
        ConfigurationUtils.setChatFontIsBold(isBold);
        ConfigurationUtils.setChatFontIsItalic(isItalic);
        ConfigurationUtils.setChatFontIsUnderline(isUnderline);
        ConfigurationUtils.setChatDefaultFontColor(color);
    }

    /**
     * Sets the font family and size
     * @param family the family name
     * @param size the size
     */
    public void setFontFamilyAndSize(String family, int size) {
        // Family
        ActionEvent evt = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, family);

        Action action = new StyledEditorKit.FontFamilyAction(family, family);
        action.actionPerformed(evt);

        // Size
        evt = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, Integer.toString(size));
        action = new StyledEditorKit.FontSizeAction(Integer.toString(size), size);
        action.actionPerformed(evt);
    }

    /**
     * Enables the bold style
     * @param b TRUE enable - FALSE disable
     */
    public void setBoldStyleEnable(boolean b) {
        StyledEditorKit editorKit = (StyledEditorKit) editorPane.getEditorKit();
        MutableAttributeSet attr = editorKit.getInputAttributes();

        if (b && !StyleConstants.isBold(attr)) {
            setStyleConstant(new HTMLEditorKit.BoldAction(), StyleConstants.Bold);
        }
    }

    /**
     * Enables the italic style
     * @param b TRUE enable - FALSE disable
     */
    public void setItalicStyleEnable(boolean b) {
        StyledEditorKit editorKit = (StyledEditorKit) editorPane.getEditorKit();
        MutableAttributeSet attr = editorKit.getInputAttributes();

        if (b && !StyleConstants.isItalic(attr)) {
            setStyleConstant(new HTMLEditorKit.ItalicAction(), StyleConstants.Italic);
        }
    }

    /**
     * Enables the underline style
     * @param b TRUE enable - FALSE disable
     */
    public void setUnderlineStyleEnable(boolean b) {
        StyledEditorKit editorKit = (StyledEditorKit) editorPane.getEditorKit();
        MutableAttributeSet attr = editorKit.getInputAttributes();

        if (b && !StyleConstants.isUnderline(attr)) {
            setStyleConstant(new HTMLEditorKit.UnderlineAction(), StyleConstants.Underline);
        }
    }

    /**
     * Sets the font color
     * @param color the color
     */
    public void setFontColor(Color color) {
        ActionEvent evt = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, "");

        Action action = new HTMLEditorKit.ForegroundAction(Integer.toString(color.getRGB()), color);

        action.actionPerformed(evt);
    }

    /**
     * Sets the given style constant.
     *
     * @param action the action
     * @param styleConstant the style constant
     */
    private void setStyleConstant(Action action, Object styleConstant) {
        ActionEvent event = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, styleConstant.toString());

        action.actionPerformed(event);
    }

    /**
     * Adds the given {@link ChatMenuListener} to this <tt>Chat</tt>.
     * The <tt>ChatMenuListener</tt> is used to determine menu elements
     * that should be added on right clicks.
     *
     * @param l the <tt>ChatMenuListener</tt> to add
     */
    public void addChatEditorMenuListener(ChatMenuListener l) {
        this.menuListeners.add(l);
    }

    /**
     * Removes the given {@link ChatMenuListener} to this <tt>Chat</tt>.
     * The <tt>ChatMenuListener</tt> is used to determine menu elements
     * that should be added on right clicks.
     *
     * @param l the <tt>ChatMenuListener</tt> to add
     */
    public void removeChatEditorMenuListener(ChatMenuListener l) {
        this.menuListeners.remove(l);
    }

    /**
     * Reloads menu.
     */
    public void loadSkin() {
        getRightButtonMenu().loadSkin();
    }

    public void changedUpdate(DocumentEvent documentevent) {
    }

    /**
     * Updates write panel size and adjusts sms properties if the sms menu
     * is visible.
     *
     * @param event the <tt>DocumentEvent</tt> that notified us
     */
    public void insertUpdate(DocumentEvent event) {
        // If we're in sms mode count the chars typed.
        if (smsButton.isVisible()) {
            updateSmsCounters(event.getDocument().getLength());
        }
    }

    /**
     * Updates write panel size and adjusts sms properties if the sms menu
     * is visible.
     *
     * @param event the <tt>DocumentEvent</tt> that notified us
     */
    public void removeUpdate(DocumentEvent event) {
        // If we're in sms mode count the chars typed.
        if (smsButton.isVisible()) {
            updateSmsCounters(event.getDocument().getLength());
        }
    }

    /**
     * Updates sms counters, 160 chars in one sms.
     * @param documentLength the current document length
     */
    private void updateSmsCounters(int documentLength) {
        smsCharCount = 160 - documentLength % 160;
        smsNumberCount = 1 + documentLength / 160;

        smsCharCountLabel.setText(String.valueOf(smsCharCount));
        smsNumberLabel.setText(String.valueOf(smsNumberCount));
    }

    /**
     * Sets the background of the write area to the specified color.
     *
     * @param color The color to set the background to.
     */
    public void setEditorPaneBackground(Color color) {
        this.centerPanel.setBackground(color);
        this.editorPane.setBackground(color);
    }

    /**
     * The task called when the current transport resource timed-out (no
     * acitivty since a long time). Then this task resets the destination to the
     * bare id.
     */
    private class OutdatedResourceTimerTask extends TimerTask {
        /**
         * The action to be performed by this timer task.
         */
        public void run() {
            outdatedResourceTimer = null;

            if (chatPanel.getChatSession() != null) {
                Iterator<ChatTransport> transports = chatPanel.getChatSession().getChatTransports();

                ChatTransport transport = null;
                while (transports.hasNext()) {
                    transport = transports.next();

                    // We found the bare ID, then set it as the current resource
                    // transport.
                    // choose only online resources
                    if (transport.getResourceName() == null && transport.getStatus().isOnline()) {
                        isOutdatedResource = false;
                        setSelectedChatTransport(transport, true);
                        return;
                    }
                }
            }

            // If there is no bare ID available, then set the current resource
            // transport as outdated.
            isOutdatedResource = true;
        }
    }

    /**
     * Initializes plug-in components for this container.
     */
    void initPluginComponents() {
        // Search for plugin components registered through the OSGI bundle
        // context.
        Collection<ServiceReference<PluginComponentFactory>> serRefs;
        String osgiFilter = "(" + net.java.sip.communicator.service.gui.Container.CONTAINER_ID + "="
                + net.java.sip.communicator.service.gui.Container.CONTAINER_CHAT_WRITE_PANEL.getID() + ")";

        try {
            serRefs = GuiActivator.bundleContext.getServiceReferences(PluginComponentFactory.class, osgiFilter);
        } catch (InvalidSyntaxException ex) {
            serRefs = null;
            logger.error("Could not obtain plugin reference.", ex);
        }
        if ((serRefs != null) && !serRefs.isEmpty()) {
            for (ServiceReference<PluginComponentFactory> serRef : serRefs) {
                PluginComponentFactory factory = GuiActivator.bundleContext.getService(serRef);
                PluginComponent component = factory.getPluginComponentInstance(this);
                this.pluginComponents.add(component);

                ChatSession chatSession = chatPanel.getChatSession();

                if (chatSession != null) {
                    ChatTransport currentTransport = chatSession.getCurrentChatTransport();
                    Object currentDescriptor = currentTransport.getDescriptor();

                    if (currentDescriptor instanceof Contact) {
                        Contact contact = (Contact) currentDescriptor;

                        component.setCurrentContact(contact, currentTransport.getResourceName());
                    }
                }

                Object c = component.getComponent();

                if (c == null)
                    continue;

                GridBagConstraints constraints = new GridBagConstraints();

                constraints.anchor = GridBagConstraints.NORTHEAST;
                constraints.fill = GridBagConstraints.NONE;
                constraints.gridy = 0;
                constraints.gridheight = 1;
                constraints.weightx = 0f;
                constraints.weighty = 0f;
                constraints.insets = new Insets(0, 3, 0, 0);

                centerPanel.add((Component) c, constraints);
            }
        }
        GuiActivator.getUIService().addPluginComponentListener(this);
        this.centerPanel.repaint();
    }

    /**
     * Indicates that a new plugin component has been added. Adds it to this
     * container if it belongs to it.
     *
     * @param event the <tt>PluginComponentEvent</tt> that notified us
     */
    public void pluginComponentAdded(PluginComponentEvent event) {
        PluginComponentFactory factory = event.getPluginComponentFactory();
        if (!factory.getContainer()
                .equals(net.java.sip.communicator.service.gui.Container.CONTAINER_CHAT_WRITE_PANEL))
            return;

        PluginComponent component = factory.getPluginComponentInstance(this);
        this.pluginComponents.add(component);

        ChatSession chatSession = chatPanel.getChatSession();
        if (chatSession != null) {
            ChatTransport currentTransport = chatSession.getCurrentChatTransport();
            Object currentDescriptor = currentTransport.getDescriptor();
            if (currentDescriptor instanceof Contact) {
                Contact contact = (Contact) currentDescriptor;

                component.setCurrentContact(contact, currentTransport.getResourceName());
            }
        }

        GridBagConstraints constraints = new GridBagConstraints();

        constraints.anchor = GridBagConstraints.NORTHEAST;
        constraints.fill = GridBagConstraints.NONE;
        constraints.gridy = 0;
        constraints.gridheight = 1;
        constraints.weightx = 0f;
        constraints.weighty = 0f;
        constraints.insets = new Insets(0, 3, 0, 0);
        centerPanel.add((Component) component.getComponent(), constraints);

        this.centerPanel.repaint();
    }

    /**
     * Removes the according plug-in component from this container.
     * 
     * @param event the <tt>PluginComponentEvent</tt> that notified us
     */
    public void pluginComponentRemoved(PluginComponentEvent event) {
        PluginComponentFactory factory = event.getPluginComponentFactory();

        if (!factory.getContainer()
                .equals(net.java.sip.communicator.service.gui.Container.CONTAINER_CHAT_WRITE_PANEL))
            return;

        Component c = (Component) factory.getPluginComponentInstance(this).getComponent();
        this.pluginComponents.remove(c);

        this.centerPanel.remove(c);
        this.centerPanel.repaint();
    }

    /**
     * Event in case of chat transport changed, for example because a different
     * transport was selected.
     *
     * @param chatSession the chat session
     */
    @Override
    public void currentChatTransportChanged(ChatSession chatSession) {
        List<PluginComponent> components;
        synchronized (this.pluginComponents) {
            components = new ArrayList<PluginComponent>(this.pluginComponents);
        }
        // determine contact instance to use in event handling when calling
        // setCurrentContact
        final Contact contact;
        final Object descriptor = chatSession.getDescriptor();
        if (descriptor instanceof MetaContact) {
            contact = ((MetaContact) descriptor).getDefaultContact();
        } else if (descriptor instanceof Contact) {
            contact = (Contact) descriptor;
        } else if (descriptor == null) {
            // In case of null contact, just call setCurrentContact for
            // null Contact and get out. Nothing else to do here.
            for (PluginComponent c : components) {
                c.setCurrentContact((Contact) null);
            }
            return;
        } else {
            logger.warn(String.format("Unsupported descriptor type %s (%s)," + "this event will not be propagated.",
                    descriptor, descriptor.getClass().getCanonicalName()));
            return;
        }
        // Call setCurrentContact on all registered pluginComponents such that
        // all get updated on the new state of the chat session
        final String resourceName = chatSession.getCurrentChatTransport().getResourceName();
        for (PluginComponent c : components) {
            try {
                c.setCurrentContact(contact, resourceName);
            } catch (RuntimeException e) {
                logger.error("BUG: setCurrentContact of PluginComponent instance: "
                        + c.getClass().getCanonicalName() + " throws a RuntimeException.", e);
            }
        }
    }

    @Override
    public void currentChatTransportUpdated(int eventID) {
        // Nothing to do here, since we do not need to communicate update events
    }
}