mirror of https://github.com/sipwise/jitsi.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1949 lines
65 KiB
1949 lines
65 KiB
/*
|
|
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
|
|
*
|
|
* Distributable under LGPL license.
|
|
* See terms of license at gnu.org.
|
|
*/
|
|
package net.java.sip.communicator.impl.gui.main.chat;
|
|
|
|
import java.awt.*;
|
|
import java.awt.datatransfer.*;
|
|
import java.awt.event.*;
|
|
import java.io.*;
|
|
import java.net.*;
|
|
import java.util.*;
|
|
import java.util.Map.Entry;
|
|
import java.util.Map;
|
|
import java.util.regex.*;
|
|
|
|
import javax.swing.*;
|
|
import javax.swing.event.*;
|
|
import javax.swing.text.*;
|
|
import javax.swing.text.html.*;
|
|
import javax.swing.text.html.HTML.*;
|
|
|
|
import net.java.sip.communicator.impl.gui.*;
|
|
import net.java.sip.communicator.impl.gui.customcontrols.*;
|
|
import net.java.sip.communicator.impl.gui.main.chat.history.*;
|
|
import net.java.sip.communicator.impl.gui.main.chat.menus.*;
|
|
import net.java.sip.communicator.impl.gui.utils.*;
|
|
import net.java.sip.communicator.service.gui.*;
|
|
import net.java.sip.communicator.service.replacement.*;
|
|
import net.java.sip.communicator.service.replacement.smilies.*;
|
|
import net.java.sip.communicator.util.*;
|
|
import net.java.sip.communicator.util.skin.*;
|
|
import net.java.sip.communicator.plugin.desktoputil.*;
|
|
import net.java.sip.communicator.plugin.desktoputil.SwingWorker;
|
|
|
|
/**
|
|
* The <tt>ChatConversationPanel</tt> is the panel, where all sent and received
|
|
* messages appear. All data is stored in an HTML document. An external CSS file
|
|
* is applied to the document to provide the look&feel. All smileys and link
|
|
* strings are processed and finally replaced by corresponding images and HTML
|
|
* links.
|
|
*
|
|
* @author Yana Stamcheva
|
|
* @author Lubomir Marinov
|
|
* @author Adam Netocny
|
|
*/
|
|
public class ChatConversationPanel
|
|
extends SCScrollPane
|
|
implements HyperlinkListener,
|
|
MouseListener,
|
|
ClipboardOwner,
|
|
Skinnable
|
|
{
|
|
/**
|
|
* The <tt>Logger</tt> used by the <tt>ChatConversationPanel</tt> class and
|
|
* its instances for logging output.
|
|
*/
|
|
private static final Logger logger
|
|
= Logger.getLogger(ChatConversationPanel.class);
|
|
|
|
/**
|
|
* The closing tag of the <code>PLAINTEXT</code> HTML element.
|
|
*/
|
|
private static final String END_PLAINTEXT_TAG = "</PLAINTEXT>";
|
|
|
|
/**
|
|
* The opening tag of the <code>PLAINTEXT</code> HTML element.
|
|
*/
|
|
private static final String START_PLAINTEXT_TAG = "<PLAINTEXT>";
|
|
|
|
/**
|
|
* The regular expression (in the form of compiled <tt>Pattern</tt>) which
|
|
* matches URLs for the purposed of turning them into links.
|
|
*/
|
|
private static final Pattern URL_PATTERN
|
|
= Pattern.compile(
|
|
"("
|
|
+ "(\\bwww\\.[^\\s<>\"]+\\.[^\\s<>\"]+/*[?#]*(\\w+[&=;?]\\w+)*\\b)" // wwwURL
|
|
+ "|"
|
|
+ "(\\bjitsi\\:[^\\s<>\"]+\\.[^\\s<>\"]*\\b)" // internalURL
|
|
+ "|"
|
|
+ "(\\b\\w+://[^\\s<>\"]+/*[?#]*(\\w+[&=;?]\\w+)*\\b)" // protocolURL
|
|
+ ")");
|
|
|
|
/**
|
|
* List for observing text messages.
|
|
*/
|
|
private Set<ChatLinkClickedListener> chatLinkClickedListeners =
|
|
new HashSet<ChatLinkClickedListener>();
|
|
|
|
/**
|
|
* The component rendering chat conversation panel text.
|
|
*/
|
|
private final JTextPane chatTextPane = new MyTextPane();
|
|
|
|
/**
|
|
* The editor kit used by the text component.
|
|
*/
|
|
private final HTMLEditorKit editorKit;
|
|
|
|
/**
|
|
* The document used by the text component.
|
|
*/
|
|
private HTMLDocument document;
|
|
|
|
/**
|
|
* The parent container.
|
|
*/
|
|
private final ChatConversationContainer chatContainer;
|
|
|
|
/**
|
|
* The menu shown on right button mouse click.
|
|
*/
|
|
private final ChatRightButtonMenu rightButtonMenu;
|
|
|
|
/**
|
|
* The currently shown href.
|
|
*/
|
|
private String currentHref;
|
|
|
|
/**
|
|
* The copy link item, contained in the right mouse click menu.
|
|
*/
|
|
private final JMenuItem copyLinkItem;
|
|
|
|
/**
|
|
* The open link item, contained in the right mouse click menu.
|
|
*/
|
|
private final JMenuItem openLinkItem;
|
|
|
|
/**
|
|
* The right mouse click menu separator.
|
|
*/
|
|
private final JSeparator copyLinkSeparator = new JSeparator();
|
|
|
|
/**
|
|
* The timestamp of the last incoming message.
|
|
*/
|
|
private long lastIncomingMsgTimestamp;
|
|
|
|
/**
|
|
* Indicates if this component is rendering a history conversation.
|
|
*/
|
|
private final boolean isHistory;
|
|
|
|
/**
|
|
* The html text content type.
|
|
*/
|
|
public static final String HTML_CONTENT_TYPE = "text/html";
|
|
|
|
/**
|
|
* The plain text content type.
|
|
*/
|
|
public static final String TEXT_CONTENT_TYPE = "text/plain";
|
|
|
|
/**
|
|
* The indicator which determines whether an automatic scroll to the bottom
|
|
* of {@link #chatTextPane} is to be performed.
|
|
*/
|
|
private boolean scrollToBottomIsPending = false;
|
|
|
|
private final static String INCOMING_MESSAGE_IMAGE_PATH
|
|
= GuiActivator.getResources().getImageURL(
|
|
"service.gui.lookandfeel.INCOMING_MESSAGE_BACKGROUND").toString();
|
|
|
|
/**
|
|
* The implementation of the routine which scrolls {@link #chatTextPane} to its
|
|
* bottom.
|
|
*/
|
|
private final Runnable scrollToBottomRunnable = new Runnable()
|
|
{
|
|
/*
|
|
* Implements Runnable#run().
|
|
*/
|
|
public void run()
|
|
{
|
|
JScrollBar verticalScrollBar = getVerticalScrollBar();
|
|
|
|
if (verticalScrollBar != null)
|
|
{
|
|
// We need to call both methods in order to be sure to scroll
|
|
// to the bottom of the text even when the user has selected
|
|
// something (changed the caret) or when a new tab has been
|
|
// added or the window has been resized.
|
|
verticalScrollBar.setValue(verticalScrollBar.getMaximum());
|
|
chatTextPane.setCaretPosition(document.getLength());
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates an instance of <tt>ChatConversationPanel</tt>.
|
|
*
|
|
* @param chatContainer The parent <tt>ChatConversationContainer</tt>.
|
|
*/
|
|
public ChatConversationPanel(ChatConversationContainer chatContainer)
|
|
{
|
|
editorKit = new SIPCommHTMLEditorKit(this);
|
|
|
|
this.chatContainer = chatContainer;
|
|
|
|
isHistory = (chatContainer instanceof HistoryWindow);
|
|
|
|
this.rightButtonMenu = new ChatRightButtonMenu(this);
|
|
|
|
this.document = (HTMLDocument) editorKit.createDefaultDocument();
|
|
|
|
this.chatTextPane.setEditorKitForContentType("text/html", editorKit);
|
|
this.chatTextPane.setEditorKit(editorKit);
|
|
this.chatTextPane.setEditable(false);
|
|
this.chatTextPane.setDocument(document);
|
|
this.chatTextPane.setDragEnabled(true);
|
|
|
|
chatTextPane.putClientProperty(
|
|
JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
|
|
Constants.loadSimpleStyle(
|
|
document.getStyleSheet(), chatTextPane.getFont());
|
|
|
|
this.chatTextPane.addHyperlinkListener(this);
|
|
this.chatTextPane.addMouseListener(this);
|
|
this.chatTextPane.setCursor(
|
|
Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
|
|
|
|
this.setWheelScrollingEnabled(true);
|
|
|
|
this.setViewportView(chatTextPane);
|
|
|
|
this.setBorder(null);
|
|
|
|
this.setHorizontalScrollBarPolicy(
|
|
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
|
|
|
ToolTipManager.sharedInstance().registerComponent(chatTextPane);
|
|
|
|
String copyLinkString
|
|
= GuiActivator.getResources().getI18NString("service.gui.COPY_LINK");
|
|
|
|
copyLinkItem
|
|
= new JMenuItem(copyLinkString,
|
|
new ImageIcon(ImageLoader.getImage(ImageLoader.COPY_ICON)));
|
|
|
|
copyLinkItem.addActionListener(new ActionListener()
|
|
{
|
|
public void actionPerformed(ActionEvent e)
|
|
{
|
|
StringSelection stringSelection = new StringSelection(
|
|
currentHref);
|
|
Clipboard clipboard = Toolkit.getDefaultToolkit()
|
|
.getSystemClipboard();
|
|
clipboard.setContents(stringSelection,
|
|
ChatConversationPanel.this);
|
|
}
|
|
});
|
|
|
|
String openLinkString
|
|
= GuiActivator.getResources().getI18NString(
|
|
"service.gui.OPEN_IN_BROWSER");
|
|
|
|
openLinkItem =
|
|
new JMenuItem(
|
|
openLinkString,
|
|
new ImageIcon(ImageLoader.getImage(ImageLoader.BROWSER_ICON)));
|
|
|
|
openLinkItem.addActionListener(new ActionListener()
|
|
{
|
|
public void actionPerformed(ActionEvent e)
|
|
{
|
|
GuiActivator.getBrowserLauncher().openURL(currentHref);
|
|
|
|
// after opening the link remove the currentHref to avoid
|
|
// clicking on the window to gain focus to open the link again
|
|
ChatConversationPanel.this.currentHref = "";
|
|
}
|
|
});
|
|
|
|
openLinkItem.setMnemonic(
|
|
GuiActivator.getResources().getI18nMnemonic(
|
|
"service.gui.OPEN_IN_BROWSER"));
|
|
|
|
copyLinkItem.setMnemonic(
|
|
GuiActivator.getResources().getI18nMnemonic(
|
|
"service.gui.COPY_LINK"));
|
|
|
|
/*
|
|
* When we append a new message (regardless of whether it is a string or
|
|
* an UI component), we want to make it visible in the viewport of this
|
|
* JScrollPane so that the user can see it.
|
|
*/
|
|
ComponentListener componentListener = new ComponentAdapter()
|
|
{
|
|
@Override
|
|
public void componentResized(ComponentEvent e)
|
|
{
|
|
synchronized (scrollToBottomRunnable)
|
|
{
|
|
if (!scrollToBottomIsPending)
|
|
return;
|
|
scrollToBottomIsPending = false;
|
|
|
|
/*
|
|
* Yana Stamcheva, pointed out that Java 5 (on Linux only?)
|
|
* needs invokeLater for JScrollBar.
|
|
*/
|
|
SwingUtilities.invokeLater(scrollToBottomRunnable);
|
|
}
|
|
}
|
|
};
|
|
|
|
chatTextPane.addComponentListener(componentListener);
|
|
getViewport().addComponentListener(componentListener);
|
|
}
|
|
|
|
/**
|
|
* Overrides Component#setBounds(int, int, int, int) in order to determine
|
|
* whether an automatic scroll of #chatTextPane to its bottom will be
|
|
* necessary at a later time in order to keep its vertical scroll bar to its
|
|
* bottom after the realization of the resize if it is at its bottom before
|
|
* the resize.
|
|
*/
|
|
@Override
|
|
public void setBounds(int x, int y, int width, int height)
|
|
{
|
|
synchronized (scrollToBottomRunnable)
|
|
{
|
|
JScrollBar verticalScrollBar = getVerticalScrollBar();
|
|
|
|
if (verticalScrollBar != null)
|
|
{
|
|
BoundedRangeModel verticalScrollBarModel
|
|
= verticalScrollBar.getModel();
|
|
|
|
if ((verticalScrollBarModel.getValue()
|
|
+ verticalScrollBarModel.getExtent()
|
|
>= verticalScrollBarModel.getMaximum())
|
|
|| !verticalScrollBar.isVisible())
|
|
scrollToBottomIsPending = true;
|
|
}
|
|
}
|
|
|
|
super.setBounds(x, y, width, height);
|
|
}
|
|
|
|
/**
|
|
* Initializes the editor by adding a header containing the date.
|
|
* TODO: remove if not used anymore
|
|
*/
|
|
// private void initEditor()
|
|
// {
|
|
// Element root = this.document.getDefaultRootElement();
|
|
//
|
|
// Date date = new Date(System.currentTimeMillis());
|
|
//
|
|
// String chatHeader = "<h1>" + GuiUtils.formatDate(date) + " " + "</h1>";
|
|
//
|
|
// try
|
|
// {
|
|
// this.document.insertAfterStart(root, chatHeader);
|
|
// }
|
|
// catch (BadLocationException e)
|
|
// {
|
|
// logger.error("Insert in the HTMLDocument failed.", e);
|
|
// }
|
|
// catch (IOException e)
|
|
// {
|
|
// logger.error("Insert in the HTMLDocument failed.", e);
|
|
// }
|
|
// }
|
|
|
|
/**
|
|
* Retrieves the contents of the sent message with the given ID.
|
|
*
|
|
* @param messageUID The ID of the message to retrieve.
|
|
* @return The contents of the message, or null if the message is not found.
|
|
*/
|
|
public String getMessageContents(String messageUID)
|
|
{
|
|
Element root = document.getDefaultRootElement();
|
|
Element e = document.getElement(root, Attribute.ID, messageUID);
|
|
if (e == null)
|
|
{
|
|
logger.warn("Could not find message with ID" + messageUID);
|
|
return null;
|
|
}
|
|
|
|
int elemLen = e.getEndOffset() - e.getStartOffset();
|
|
String res = null;
|
|
try
|
|
{
|
|
res = document.getText(e.getStartOffset(), elemLen);
|
|
}
|
|
catch (BadLocationException exc)
|
|
{
|
|
logger.warn("Could not get message contents for message "
|
|
+ "with ID" + messageUID, exc);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Creates a tag that shows the last edit time of a message, in the format
|
|
* (Edited at ...).
|
|
* If <tt>date < 0</tt>, returns an empty tag that serves as a placeholder
|
|
* for future corrections of this message.
|
|
*
|
|
* @param messageUID The ID of the edited message.
|
|
* @param date The date when the message was last edited, or -1 to generate
|
|
* an empty tag.
|
|
* @return The string representation of the tag.
|
|
*/
|
|
private String generateEditedAtTag(String messageUID, long date)
|
|
{
|
|
StringBuilder res = new StringBuilder();
|
|
// Use a <cite /> tag here as most of the other inline tags (e.g. h1-7,
|
|
// b, i) cause different problems when used in setOuterHTML.
|
|
res.append("<cite id='");
|
|
res.append(messageUID);
|
|
res.append("-editedAt'> ");
|
|
if (date > 0)
|
|
{
|
|
res.append(" ");
|
|
String contents = GuiActivator.getResources().getI18NString(
|
|
"service.gui.EDITED_AT",
|
|
new String[] { GuiUtils.formatTime(date) }
|
|
);
|
|
res.append(contents);
|
|
}
|
|
res.append("</cite>");
|
|
return res.toString();
|
|
}
|
|
|
|
/**
|
|
* Processes the message given by the parameters.
|
|
*
|
|
* @param chatMessage the message
|
|
* @param keyword a substring of <tt>chatMessage</tt> to be highlighted upon
|
|
* display of <tt>chatMessage</tt> in the UI
|
|
* @return the processed message
|
|
*/
|
|
public String processMessage(ChatMessage chatMessage, String keyword)
|
|
{
|
|
String contactName = chatMessage.getContactName();
|
|
String contactDisplayName = chatMessage.getContactDisplayName();
|
|
if (contactDisplayName == null
|
|
|| contactDisplayName.trim().length() <= 0)
|
|
contactDisplayName = contactName;
|
|
|
|
String contentType = chatMessage.getContentType();
|
|
long date = chatMessage.getDate();
|
|
String messageType = chatMessage.getMessageType();
|
|
String messageTitle = chatMessage.getMessageTitle();
|
|
String message = chatMessage.getMessage();
|
|
String messageUID = chatMessage.getMessageUID();
|
|
|
|
String msgID = "message";
|
|
String msgHeaderID = "messageHeader";
|
|
String chatString = "";
|
|
String endHeaderTag = "";
|
|
String dateString = getDateString(date);
|
|
String idAttr = messageUID == null ? "" : " id='" + messageUID + "'";
|
|
String dateAttr = " date='" + date + "'";
|
|
String editedAtTag = generateEditedAtTag(messageUID, -1);
|
|
|
|
String startHistoryDivTag
|
|
= "<DIV identifier=\"" + msgID + "\" style=\"color:#707070;\">";
|
|
String startSystemDivTag
|
|
= "<DIV identifier=\"systemMessage\" style=\"color:#627EB7;\">";
|
|
String endDivTag = "</DIV>";
|
|
|
|
String startPlainTextTag;
|
|
String endPlainTextTag;
|
|
|
|
if (HTML_CONTENT_TYPE.equals(contentType))
|
|
{
|
|
startPlainTextTag = "";
|
|
endPlainTextTag = "";
|
|
}
|
|
else
|
|
{
|
|
startPlainTextTag = START_PLAINTEXT_TAG;
|
|
endPlainTextTag = END_PLAINTEXT_TAG;
|
|
}
|
|
|
|
if (messageType.equals(Chat.INCOMING_MESSAGE))
|
|
{
|
|
this.lastIncomingMsgTimestamp = System.currentTimeMillis();
|
|
|
|
StringBuffer headerBuffer = new StringBuffer();
|
|
|
|
headerBuffer.append("<h2 identifier=\"" + msgHeaderID + "\"");
|
|
headerBuffer.append(dateAttr + ">");
|
|
headerBuffer.append("<a style=\"color:#488fe7;");
|
|
headerBuffer.append("font-weight:bold;");
|
|
headerBuffer.append("text-decoration:none;\" ");
|
|
headerBuffer.append("href=\"" + contactName + "\">");
|
|
headerBuffer.append(dateString + contactDisplayName + " at "
|
|
+ GuiUtils.formatTime(date) + editedAtTag);
|
|
headerBuffer.append("</a></h2>");
|
|
|
|
chatString = createIncomingMessageTag(msgID + "\"" + idAttr,
|
|
headerBuffer.toString(),
|
|
startPlainTextTag
|
|
+ formatMessage(message, contentType, keyword)
|
|
+ endPlainTextTag);
|
|
System.out.println("CHAT STRING OSHTE TUUUUUK=======" + chatString);
|
|
// chatString = "<h2 identifier=\"" + msgHeaderID + "\""
|
|
// + " date=\"" + date + "\">"
|
|
// + "<a style=\"color:#488fe7;"
|
|
// + "font-weight:bold;"
|
|
// + "text-decoration:none;\" "
|
|
// + "href=\"" + contactName + "\">";
|
|
//
|
|
// endHeaderTag = "</a></h2>";
|
|
|
|
// String startDivTag = "<DIV identifier=\"" + msgID
|
|
// + "\" style=\""+ createIncomingMessageStyle()+"\">";
|
|
//
|
|
// chatString
|
|
// += dateString + contactDisplayName + " at "
|
|
// + GuiUtils.formatTime(date)
|
|
// + endHeaderTag + startDivTag + startPlainTextTag
|
|
// + formatMessage(message, contentType, keyword)
|
|
// + endPlainTextTag + endDivTag;
|
|
|
|
}
|
|
else if (messageType.equals(Chat.SMS_MESSAGE))
|
|
{
|
|
chatString = "<h2 identifier=\""
|
|
+ msgHeaderID
|
|
+ "\" date=\""
|
|
+ date + "\">";
|
|
|
|
endHeaderTag = "</h2>";
|
|
|
|
String startDivTag = "<DIV identifier=\"" + msgID
|
|
+ "\" style=\""+ createSmsMessageStyle()+"\">";
|
|
|
|
chatString
|
|
+= "SMS: " + dateString + contactName + " at "
|
|
+ GuiUtils.formatTime(date) + endHeaderTag + startDivTag
|
|
+ startPlainTextTag
|
|
+ formatMessage(message, contentType, keyword)
|
|
+ endPlainTextTag + endDivTag;
|
|
}
|
|
else if (messageType.equals(Chat.OUTGOING_MESSAGE))
|
|
{
|
|
chatString = "<h3 identifier=\"" + msgHeaderID + "\""
|
|
+ dateAttr + ">"
|
|
+ "<a style=\"color:#6a6868;"
|
|
+ "font-weight:bold;"
|
|
+ "text-decoration:none;\" "
|
|
+ "href=\"" + contactName + "\">";
|
|
|
|
endHeaderTag = "</a></h3>";
|
|
|
|
String startDivTag = "<DIV identifier=\"" + msgID
|
|
+ "\" style=\""+ createOutgoingMessageStyle()+"\">";
|
|
|
|
chatString
|
|
+= dateString + contactDisplayName + " at "
|
|
+ GuiUtils.formatTime(date) + editedAtTag + endHeaderTag
|
|
+ startDivTag + startPlainTextTag
|
|
+ formatMessage(message, contentType, keyword)
|
|
+ endPlainTextTag + endDivTag;
|
|
}
|
|
else if (messageType.equals(Chat.STATUS_MESSAGE))
|
|
{
|
|
chatString = "<h4 identifier=\"statusMessage\" date=\""
|
|
+ date + "\">";
|
|
endHeaderTag = "</h4>";
|
|
|
|
chatString
|
|
+= GuiUtils.formatTime(date) + " " + contactName + " " + message
|
|
+ endHeaderTag;
|
|
}
|
|
else if (messageType.equals(Chat.ACTION_MESSAGE))
|
|
{
|
|
chatString = "<p identifier=\"actionMessage\" date=\""
|
|
+ date + "\">";
|
|
endHeaderTag = "</p>";
|
|
|
|
chatString += "* " + GuiUtils.formatTime(date)
|
|
+ " " + contactName + " "
|
|
+ message
|
|
+ endHeaderTag;
|
|
}
|
|
else if (messageType.equals(Chat.SYSTEM_MESSAGE))
|
|
{
|
|
chatString
|
|
+= startSystemDivTag + startPlainTextTag
|
|
+ formatMessage(message, contentType, keyword)
|
|
+ endPlainTextTag + endDivTag;
|
|
}
|
|
else if (messageType.equals(Chat.ERROR_MESSAGE))
|
|
{
|
|
chatString = "<h6 identifier=\""
|
|
+ msgHeaderID
|
|
+ "\" date=\""
|
|
+ date + "\">";
|
|
|
|
endHeaderTag = "</h6>";
|
|
|
|
String errorIcon = "<IMG SRC='"
|
|
+ ImageLoader.getImageUri(ImageLoader.EXCLAMATION_MARK)
|
|
+ "' </IMG>";
|
|
|
|
chatString += errorIcon
|
|
+ messageTitle
|
|
+ endHeaderTag + "<h5>" + message + "</h5>";
|
|
}
|
|
else if (messageType.equals(Chat.HISTORY_INCOMING_MESSAGE))
|
|
{
|
|
chatString = "<h2 identifier=\"" + msgHeaderID + "\""
|
|
+ dateAttr + ">"
|
|
+ "<a style=\"color:#488fe7;"
|
|
+ "font-weight:bold;"
|
|
+ "text-decoration:none;\" "
|
|
+ "href=\"" + contactName + "\">";
|
|
|
|
endHeaderTag = "</a></h2>";
|
|
|
|
chatString
|
|
+= dateString + contactDisplayName
|
|
+ " at " + GuiUtils.formatTime(date) + endHeaderTag
|
|
+ editedAtTag + startHistoryDivTag + startPlainTextTag
|
|
+ formatMessage(message, contentType, keyword)
|
|
+ endPlainTextTag + endDivTag;
|
|
}
|
|
else if (messageType.equals(Chat.HISTORY_OUTGOING_MESSAGE))
|
|
{
|
|
chatString = "<h3 identifier=\"" + msgHeaderID + "\""
|
|
+ dateAttr + ">"
|
|
+ "<a style=\"color:#6a6868;"
|
|
+ "font-weight:bold;"
|
|
+ "text-decoration:none;\" "
|
|
+ "href=\"" + contactName + "\">";
|
|
|
|
endHeaderTag = "</h3>";
|
|
|
|
chatString
|
|
+= dateString
|
|
+ contactDisplayName + " at " + GuiUtils.formatTime(date)
|
|
+ editedAtTag + endHeaderTag
|
|
+ startHistoryDivTag + startPlainTextTag
|
|
+ formatMessage(message, contentType, keyword)
|
|
+ endPlainTextTag + endDivTag;
|
|
}
|
|
|
|
return chatString;
|
|
}
|
|
|
|
/**
|
|
* Processes the message given by the parameters.
|
|
*
|
|
* @param chatMessage the message.
|
|
* @return the formatted message
|
|
*/
|
|
public String processMessage(ChatMessage chatMessage)
|
|
{
|
|
return processMessage(chatMessage, null);
|
|
}
|
|
|
|
/**
|
|
* Replaces the contents of the message with ID of the corrected message
|
|
* specified in chatMessage, with this message.
|
|
*
|
|
* @param chatMessage A <tt>ChatMessage</tt> that contains all the required
|
|
* information to correct the old message.
|
|
*/
|
|
public void correctMessage(ChatMessage chatMessage)
|
|
{
|
|
String correctedUID = chatMessage.getCorrectedMessageUID();
|
|
Element root = document.getDefaultRootElement();
|
|
Element e = document.getElement(root, Attribute.ID, correctedUID);
|
|
if (e == null)
|
|
{
|
|
logger.warn("Could not find message with ID " + correctedUID);
|
|
return;
|
|
}
|
|
int len = e.getEndOffset() - e.getStartOffset();
|
|
|
|
StringBuilder newContents = new StringBuilder();
|
|
String bgColor = GuiActivator.getResources().getColorString(
|
|
"service.gui.CHAT_EDIT_MESSAGE_BACKGROUND");
|
|
newContents.append("<div identifier='message' id='");
|
|
newContents.append(chatMessage.getMessageUID());
|
|
newContents.append("' bgcolor='");
|
|
newContents.append(bgColor);
|
|
newContents.append("'>");
|
|
if (chatMessage.getContentType().equals(TEXT_CONTENT_TYPE))
|
|
{
|
|
newContents.append(START_PLAINTEXT_TAG);
|
|
newContents.append(chatMessage.getMessage());
|
|
newContents.append(END_PLAINTEXT_TAG);
|
|
}
|
|
else
|
|
{
|
|
newContents.append(chatMessage.getMessage());
|
|
}
|
|
newContents.append("</div>");
|
|
|
|
Element header = document.getElement(root, Attribute.ID,
|
|
correctedUID + "-editedAt");
|
|
|
|
try
|
|
{
|
|
if (header != null)
|
|
{
|
|
String newHeaderContents = generateEditedAtTag(
|
|
chatMessage.getMessageUID(), chatMessage.getDate());
|
|
document.setOuterHTML(header, newHeaderContents);
|
|
}
|
|
document.setOuterHTML(e, newContents.toString());
|
|
}
|
|
catch (BadLocationException ex)
|
|
{
|
|
logger.error("Could not replace chat message", ex);
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
logger.error("Could not replace chat message", ex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Appends the given string at the end of the contained in this panel
|
|
* document.
|
|
*
|
|
* @param chatString the string to append
|
|
*/
|
|
public void appendMessageToEnd(String chatString, String contentType)
|
|
{
|
|
synchronized (scrollToBottomRunnable)
|
|
{
|
|
Element root = document.getDefaultRootElement();
|
|
|
|
// Need to call explicitly scrollToBottom, because for some
|
|
// reason the componentResized event isn't fired every time we
|
|
// add text.
|
|
// Replaced by the code on line: 573.
|
|
//
|
|
// scrollToBottomIsPending = true;
|
|
System.out.println("CHAT STRING+=========" + chatString);
|
|
try
|
|
{
|
|
document
|
|
.insertAfterEnd(
|
|
root.getElement(root.getElementCount() - 1),
|
|
chatString);
|
|
|
|
// Need to call explicitly scrollToBottom, because for some
|
|
// reason the componentResized event isn't fired every time we
|
|
// add text.
|
|
SwingUtilities.invokeLater(scrollToBottomRunnable);
|
|
}
|
|
catch (BadLocationException e)
|
|
{
|
|
logger.error("Insert in the HTMLDocument failed.", e);
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
logger.error("Insert in the HTMLDocument failed.", e);
|
|
}
|
|
if (!isHistory)
|
|
ensureDocumentSize();
|
|
|
|
// Process replacements.
|
|
final Element elem;
|
|
/*
|
|
* Check to make sure element isn't the first element in the HTML
|
|
* document.
|
|
*/
|
|
if (!(root.getElementCount() < 2))
|
|
{
|
|
elem = root.getElement(root.getElementCount() - 2);
|
|
}
|
|
else
|
|
elem = root.getElement(1);
|
|
|
|
/*
|
|
* Replacements will be processed only if it is enabled in the
|
|
* property
|
|
*/
|
|
if (GuiActivator.getConfigurationService().getBoolean(
|
|
ReplacementProperty.REPLACEMENT_ENABLE, true)
|
|
|| GuiActivator.getConfigurationService().getBoolean(
|
|
ReplacementProperty.getPropertyName("SMILEY"), true))
|
|
{
|
|
processReplacement(elem, chatString, contentType);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats the given message. Processes the messages and replaces links to
|
|
* video/image sources with their previews or any other substitution. Spawns
|
|
* a separate thread for replacement.
|
|
*
|
|
* @param elem the element in the HTML Document.
|
|
* @param chatString the message.
|
|
*/
|
|
private void processReplacement(final Element elem,
|
|
final String chatString,
|
|
final String contentType)
|
|
{
|
|
final String chatFinal = chatString;
|
|
|
|
SwingWorker worker = new SwingWorker()
|
|
{
|
|
public Object construct() throws Exception
|
|
{
|
|
String temp = "", msgStore = chatFinal;
|
|
|
|
boolean isEnabled
|
|
= GuiActivator.getConfigurationService().getBoolean(
|
|
ReplacementProperty.REPLACEMENT_ENABLE, true);
|
|
|
|
Map<String, ReplacementService> listSources
|
|
= GuiActivator.getReplacementSources();
|
|
|
|
Iterator<Entry<String, ReplacementService>> entrySetIter
|
|
= listSources.entrySet().iterator();
|
|
|
|
for (int i = 0; i < listSources.size(); i++)
|
|
{
|
|
Map.Entry<String, ReplacementService> entry
|
|
= entrySetIter.next();
|
|
|
|
ReplacementService source = entry.getValue();
|
|
|
|
boolean isSmiley
|
|
= source instanceof SmiliesReplacementService;
|
|
|
|
if (!(GuiActivator.getConfigurationService().getBoolean(
|
|
ReplacementProperty.getPropertyName(source
|
|
.getSourceName()), true) && (isEnabled || isSmiley)))
|
|
continue;
|
|
|
|
String sourcePattern = source.getPattern();
|
|
Pattern p = Pattern.compile(sourcePattern,
|
|
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
|
|
|
Matcher m = p.matcher(msgStore);
|
|
|
|
String startPlainTextTag = "";
|
|
String endPlainTextTag = "";
|
|
|
|
if (!HTML_CONTENT_TYPE.equals(contentType))
|
|
{
|
|
startPlainTextTag = START_PLAINTEXT_TAG;
|
|
endPlainTextTag = END_PLAINTEXT_TAG;
|
|
}
|
|
|
|
int count = 0, startPos = 0;
|
|
StringBuffer msgBuff = new StringBuffer();
|
|
|
|
while (m.find())
|
|
{
|
|
count++;
|
|
msgBuff.append(msgStore.substring(startPos, m.start()));
|
|
startPos = m.end();
|
|
|
|
temp = source.getReplacement(m.group());
|
|
|
|
if(!temp.equals(m.group(0)) || source.getSourceName()
|
|
.equals("DIRECTIMAGE"))
|
|
{
|
|
if(isSmiley)
|
|
{
|
|
msgBuff.append(endPlainTextTag);
|
|
msgBuff.append("<IMG SRC=\"");
|
|
}
|
|
else
|
|
{
|
|
msgBuff.append(
|
|
"<IMG HEIGHT=\"90\" WIDTH=\"120\" SRC=\"");
|
|
}
|
|
|
|
msgBuff.append(temp);
|
|
msgBuff.append("\" BORDER=\"0\" ALT=\"");
|
|
msgBuff.append(m.group(0));
|
|
msgBuff.append("\"></IMG>");
|
|
|
|
if(isSmiley)
|
|
msgBuff.append(startPlainTextTag);
|
|
}
|
|
else
|
|
{
|
|
msgBuff.append(
|
|
msgStore.substring(m.start(), m.end()));
|
|
}
|
|
}
|
|
|
|
msgBuff.append(msgStore.substring(startPos));
|
|
|
|
/*
|
|
* replace the msgStore variable with the current replaced
|
|
* message before next iteration
|
|
*/
|
|
if (!msgBuff.toString().equals(msgStore))
|
|
{
|
|
msgStore = msgBuff.toString();
|
|
}
|
|
}
|
|
|
|
if (!msgStore.equals(chatFinal))
|
|
{
|
|
synchronized (scrollToBottomRunnable)
|
|
{
|
|
scrollToBottomIsPending = true;
|
|
document.setOuterHTML(elem, msgStore.toString()
|
|
.substring(msgStore.indexOf("<DIV")));
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
};
|
|
worker.start();
|
|
}
|
|
|
|
/**
|
|
* Ensures that the document won't become too big. When the document reaches
|
|
* a certain size the first message in the page is removed.
|
|
*/
|
|
private void ensureDocumentSize()
|
|
{
|
|
if (document.getLength() > Chat.CHAT_BUFFER_SIZE)
|
|
{
|
|
int msgElementCount = 0;
|
|
|
|
Element firstMsgElement = null;
|
|
|
|
int firstMsgIndex = 0;
|
|
|
|
Element rootElement = this.document.getDefaultRootElement();
|
|
// Count how many messages we have in the document.
|
|
for (int i = 0; i < rootElement.getElementCount(); i++)
|
|
{
|
|
String idAttr = (String) rootElement.getElement(i)
|
|
.getAttributes().getAttribute("identifier");
|
|
|
|
if(idAttr != null
|
|
&& (idAttr.equals("message")
|
|
|| idAttr.equals("statusMessage")
|
|
|| idAttr.equals("systemMessage")))
|
|
{
|
|
if(firstMsgElement == null)
|
|
{
|
|
firstMsgElement = rootElement.getElement(i);
|
|
firstMsgIndex = i;
|
|
}
|
|
|
|
msgElementCount++;
|
|
}
|
|
}
|
|
|
|
// If we doesn't have any known elements in the document or if we
|
|
// have only one long message we don't want to remove it.
|
|
if(firstMsgElement == null || msgElementCount < 2)
|
|
return;
|
|
|
|
try
|
|
{
|
|
// Remove the header of the message if such exists.
|
|
if(firstMsgIndex > 0)
|
|
{
|
|
Element headerElement = rootElement.getElement(firstMsgIndex - 1);
|
|
|
|
String idAttr = (String) headerElement
|
|
.getAttributes().getAttribute("identifier");
|
|
|
|
if(idAttr != null && idAttr.equals("messageHeader"))
|
|
{
|
|
this.document.remove(headerElement.getStartOffset(),
|
|
headerElement.getEndOffset()
|
|
- headerElement.getStartOffset());
|
|
}
|
|
}
|
|
|
|
// Remove the message itself.
|
|
this.document.remove(firstMsgElement.getStartOffset(),
|
|
firstMsgElement.getEndOffset()
|
|
- firstMsgElement.getStartOffset());
|
|
}
|
|
catch (BadLocationException e)
|
|
{
|
|
logger.error("Error removing messages from chat: ", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Highlights keywords searched in the history.
|
|
*
|
|
* @param message the source message
|
|
* @param contentType the content type
|
|
* @param keyword the searched keyword
|
|
* @return the formatted message
|
|
*/
|
|
private String processKeyword( String message,
|
|
String contentType,
|
|
String keyword)
|
|
{
|
|
String startPlainTextTag;
|
|
String endPlainTextTag;
|
|
|
|
if (HTML_CONTENT_TYPE.equals(contentType))
|
|
{
|
|
startPlainTextTag = "";
|
|
endPlainTextTag = "";
|
|
}
|
|
else
|
|
{
|
|
startPlainTextTag = START_PLAINTEXT_TAG;
|
|
endPlainTextTag = END_PLAINTEXT_TAG;
|
|
}
|
|
|
|
Matcher m
|
|
= Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE)
|
|
.matcher(message);
|
|
StringBuffer msgBuffer = new StringBuffer();
|
|
int prevEnd = 0;
|
|
|
|
while (m.find())
|
|
{
|
|
msgBuffer.append(message.substring(prevEnd, m.start()));
|
|
prevEnd = m.end();
|
|
|
|
String keywordMatch = m.group().trim();
|
|
|
|
msgBuffer.append(endPlainTextTag);
|
|
msgBuffer.append("<b>");
|
|
msgBuffer.append(keywordMatch);
|
|
msgBuffer.append("</b>");
|
|
msgBuffer.append(startPlainTextTag);
|
|
}
|
|
|
|
/*
|
|
* If the keyword didn't match, let the outside world be able to
|
|
* discover it.
|
|
*/
|
|
if (prevEnd == 0)
|
|
return message;
|
|
|
|
msgBuffer.append(message.substring(prevEnd));
|
|
return msgBuffer.toString();
|
|
}
|
|
|
|
/**
|
|
* Formats the given message. Processes all smiley chars, new lines and
|
|
* links.
|
|
*
|
|
* @param message the message to be formatted
|
|
* @param contentType the content type of the message to be formatted
|
|
* @param keyword the word to be highlighted
|
|
* @return the formatted message
|
|
*/
|
|
private String formatMessage(String message,
|
|
String contentType,
|
|
String keyword)
|
|
{
|
|
// If the message content type is HTML we won't process links and
|
|
// new lines, but only the smileys.
|
|
if (!HTML_CONTENT_TYPE.equals(contentType))
|
|
{
|
|
|
|
/*
|
|
* We disallow HTML in plain-text messages. But processKeyword
|
|
* introduces HTML. So we'll allow HTML if processKeyword has
|
|
* introduced it in order to not break highlighting.
|
|
*/
|
|
boolean processHTMLChars;
|
|
|
|
if ((keyword != null) && (keyword.length() != 0))
|
|
{
|
|
String messageWithProcessedKeyword
|
|
= processKeyword(message, contentType, keyword);
|
|
|
|
/*
|
|
* The same String instance will be returned if there was no
|
|
* keyword match. Calling #equals() is expensive so == is
|
|
* intentional.
|
|
*/
|
|
processHTMLChars = (messageWithProcessedKeyword == message);
|
|
message = messageWithProcessedKeyword;
|
|
}
|
|
else
|
|
processHTMLChars = true;
|
|
|
|
message
|
|
= processNewLines(
|
|
processLinksAndHTMLChars(message, processHTMLChars));
|
|
}
|
|
// If the message content is HTML, we process br and img tags.
|
|
else
|
|
{
|
|
if ((keyword != null) && (keyword.length() != 0))
|
|
message = processKeyword(message, contentType, keyword);
|
|
message = processImgTags(processBrTags(message));
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Formats all links in a given message and optionally escapes special HTML
|
|
* characters such as <, >, & and " in order to prevent HTML
|
|
* injection in plain-text messages such as writing
|
|
* <code></PLAINTEXT></code>, HTML which is going to be rendered as
|
|
* such and <code><PLAINTEXT></code>. The two procedures are carried
|
|
* out in one call in order to not break URLs which contain special HTML
|
|
* characters such as &.
|
|
*
|
|
* @param message The source message string.
|
|
* @param processHTMLChars <tt>true</tt> to escape the special HTML chars;
|
|
* otherwise, <tt>false</tt>
|
|
* @return The message string with properly formatted links.
|
|
*/
|
|
private String processLinksAndHTMLChars(String message,
|
|
boolean processHTMLChars)
|
|
{
|
|
Matcher m = URL_PATTERN.matcher(message);
|
|
StringBuffer msgBuffer = new StringBuffer();
|
|
int prevEnd = 0;
|
|
|
|
while (m.find())
|
|
{
|
|
String fromPrevEndToStart = message.substring(prevEnd, m.start());
|
|
|
|
if (processHTMLChars)
|
|
fromPrevEndToStart = processHTMLChars(fromPrevEndToStart);
|
|
msgBuffer.append(fromPrevEndToStart);
|
|
prevEnd = m.end();
|
|
|
|
String url = m.group().trim();
|
|
|
|
msgBuffer.append(END_PLAINTEXT_TAG);
|
|
msgBuffer.append("<A href=\"");
|
|
if (url.startsWith("www"))
|
|
msgBuffer.append("http://");
|
|
msgBuffer.append(url);
|
|
msgBuffer.append("\">");
|
|
msgBuffer.append(url);
|
|
msgBuffer.append("</A>");
|
|
msgBuffer.append(START_PLAINTEXT_TAG);
|
|
}
|
|
|
|
String fromPrevEndToEnd = message.substring(prevEnd);
|
|
|
|
if (processHTMLChars)
|
|
fromPrevEndToEnd = processHTMLChars(fromPrevEndToEnd);
|
|
msgBuffer.append(fromPrevEndToEnd);
|
|
|
|
return msgBuffer.toString();
|
|
}
|
|
|
|
/**
|
|
* Escapes special HTML characters such as <, >, & and " in
|
|
* the specified message.
|
|
*
|
|
* @param message the message to be processed
|
|
* @return the processed message with escaped special HTML characters
|
|
*/
|
|
private String processHTMLChars(String message)
|
|
{
|
|
return
|
|
message
|
|
.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace("\"", """);
|
|
}
|
|
|
|
/**
|
|
* Formats message new lines.
|
|
*
|
|
* @param message The source message string.
|
|
* @return The message string with properly formatted new lines.
|
|
*/
|
|
private String processNewLines(String message)
|
|
{
|
|
|
|
/*
|
|
* <br> tags are needed to visualize a new line in the html format, but
|
|
* when copied to the clipboard they are exported to the plain text
|
|
* format as ' ' and not as '\n'.
|
|
*
|
|
* See bug N4988885:
|
|
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4988885
|
|
*
|
|
* To fix this we need " " - the HTML-Code for ASCII-Character No.10
|
|
* (Line feed).
|
|
*/
|
|
return
|
|
message
|
|
.replaceAll(
|
|
"\n",
|
|
END_PLAINTEXT_TAG + "<BR/> " + START_PLAINTEXT_TAG);
|
|
}
|
|
|
|
/**
|
|
* Opens a link in the default browser when clicked and shows link url in a
|
|
* popup on mouseover.
|
|
*
|
|
* @param e The HyperlinkEvent.
|
|
*/
|
|
public void hyperlinkUpdate(HyperlinkEvent e)
|
|
{
|
|
if (e.getEventType() == HyperlinkEvent.EventType.ENTERED)
|
|
{
|
|
String href = e.getDescription();
|
|
|
|
this.currentHref = href;
|
|
}
|
|
else if (e.getEventType() == HyperlinkEvent.EventType.EXITED)
|
|
{
|
|
this.currentHref = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the text pane of this conversation panel.
|
|
*
|
|
* @return The text pane of this conversation panel.
|
|
*/
|
|
public JTextPane getChatTextPane()
|
|
{
|
|
return chatTextPane;
|
|
}
|
|
|
|
/**
|
|
* Returns the time of the last received message.
|
|
*
|
|
* @return The time of the last received message.
|
|
*/
|
|
public long getLastIncomingMsgTimestamp()
|
|
{
|
|
return lastIncomingMsgTimestamp;
|
|
}
|
|
|
|
/**
|
|
* When a right button click is performed in the editor pane, a popup menu
|
|
* is opened.
|
|
* In case of the Scheme being internal, it won't open the Browser but
|
|
* instead it will trigger the forwarded action.
|
|
*
|
|
* @param e The MouseEvent.
|
|
*/
|
|
public void mouseClicked(MouseEvent e)
|
|
{
|
|
Point p = e.getPoint();
|
|
SwingUtilities.convertPointToScreen(p, e.getComponent());
|
|
|
|
if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0
|
|
|| (e.isControlDown() && !e.isMetaDown()))
|
|
{
|
|
openContextMenu(p);
|
|
}
|
|
else if ((e.getModifiers() & InputEvent.BUTTON1_MASK) != 0
|
|
&& currentHref != null && currentHref.length() != 0)
|
|
{
|
|
URI uri;
|
|
try
|
|
{
|
|
uri = new URI(currentHref);
|
|
}
|
|
catch (URISyntaxException e1)
|
|
{
|
|
logger.error("Invalid URL", e1);
|
|
return;
|
|
}
|
|
if(uri.getScheme().equals("jitsi"))
|
|
{
|
|
for(ChatLinkClickedListener l:chatLinkClickedListeners)
|
|
{
|
|
l.chatLinkClicked(uri);
|
|
}
|
|
}
|
|
else
|
|
GuiActivator.getBrowserLauncher().openURL(currentHref);
|
|
|
|
// after opening the link remove the currentHref to avoid
|
|
// clicking on the window to gain focus to open the link again
|
|
this.currentHref = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens this panel context menu at the given point.
|
|
*
|
|
* @param p the point where to position the left-top cornet of the context
|
|
* menu
|
|
*/
|
|
private void openContextMenu(Point p)
|
|
{
|
|
if (currentHref != null && currentHref.length() != 0
|
|
&& !currentHref.startsWith("jitsi://"))
|
|
{
|
|
rightButtonMenu.insert(openLinkItem, 0);
|
|
rightButtonMenu.insert(copyLinkItem, 1);
|
|
rightButtonMenu.insert(copyLinkSeparator, 2);
|
|
}
|
|
else
|
|
{
|
|
rightButtonMenu.remove(openLinkItem);
|
|
rightButtonMenu.remove(copyLinkItem);
|
|
rightButtonMenu.remove(copyLinkSeparator);
|
|
}
|
|
|
|
if (chatTextPane.getSelectedText() != null)
|
|
{
|
|
rightButtonMenu.enableCopy();
|
|
}
|
|
else
|
|
{
|
|
rightButtonMenu.disableCopy();
|
|
}
|
|
rightButtonMenu.setInvoker(chatTextPane);
|
|
rightButtonMenu.setLocation(p.x, p.y);
|
|
rightButtonMenu.setVisible(true);
|
|
}
|
|
|
|
public void mousePressed(MouseEvent e) {}
|
|
|
|
public void mouseReleased(MouseEvent e) {}
|
|
|
|
public void mouseEntered(MouseEvent e) {}
|
|
|
|
public void mouseExited(MouseEvent e) {}
|
|
|
|
public void lostOwnership(Clipboard clipboard, Transferable contents) {}
|
|
|
|
/**
|
|
* Returns the chat container.
|
|
*
|
|
* @return the chat container
|
|
*/
|
|
public ChatConversationContainer getChatContainer()
|
|
{
|
|
return chatContainer;
|
|
}
|
|
|
|
/**
|
|
* Copies the selected conversation panel content to the clipboard.
|
|
*/
|
|
public void copyConversation()
|
|
{
|
|
this.chatTextPane.copy();
|
|
}
|
|
|
|
/**
|
|
* Creates new document and all the messages that will be processed in the
|
|
* future will be appended in it.
|
|
*/
|
|
public void clear()
|
|
{
|
|
this.document = (HTMLDocument) editorKit.createDefaultDocument();
|
|
Constants.loadSimpleStyle(
|
|
document.getStyleSheet(), chatTextPane.getFont());
|
|
}
|
|
|
|
/**
|
|
* Sets the given document to the editor pane in this panel.
|
|
*
|
|
* @param document the document to set
|
|
*/
|
|
public void setContent(HTMLDocument document)
|
|
{
|
|
synchronized (scrollToBottomRunnable)
|
|
{
|
|
scrollToBottomIsPending = true;
|
|
|
|
this.document = document;
|
|
chatTextPane.setDocument(this.document);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the default document contained in this panel, created on init or
|
|
* when clear is invoked.
|
|
*/
|
|
public void setDefaultContent()
|
|
{
|
|
setContent(document);
|
|
}
|
|
|
|
/**
|
|
* Returns the document contained in this panel.
|
|
*
|
|
* @return the document contained in this panel
|
|
*/
|
|
public HTMLDocument getContent()
|
|
{
|
|
return (HTMLDocument) this.chatTextPane.getDocument();
|
|
}
|
|
|
|
/**
|
|
* Returns the right button popup menu.
|
|
*
|
|
* @return the right button popup menu
|
|
*/
|
|
public ChatRightButtonMenu getRightButtonMenu()
|
|
{
|
|
return rightButtonMenu;
|
|
}
|
|
|
|
/**
|
|
* Returns the date of the first message in the current page.
|
|
*
|
|
* @return the date of the first message in the current page
|
|
*/
|
|
public Date getPageFirstMsgTimestamp()
|
|
{
|
|
Element rootElement = this.document.getDefaultRootElement();
|
|
|
|
Element firstMessageElement = null;
|
|
|
|
for(int i = 0; i < rootElement.getElementCount(); i ++)
|
|
{
|
|
String idAttr = (String) rootElement.getElement(i)
|
|
.getAttributes().getAttribute("identifier");
|
|
|
|
if (idAttr != null && idAttr.equals("messageHeader"))
|
|
{
|
|
firstMessageElement = rootElement.getElement(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(firstMessageElement == null)
|
|
return new Date(Long.MAX_VALUE);
|
|
|
|
String dateObject = firstMessageElement
|
|
.getAttributes().getAttribute("date").toString();
|
|
|
|
return new Date(Long.parseLong(dateObject));
|
|
}
|
|
|
|
/**
|
|
* Returns the date of the last message in the current page.
|
|
*
|
|
* @return the date of the last message in the current page
|
|
*/
|
|
public Date getPageLastMsgTimestamp()
|
|
{
|
|
Element rootElement = this.document.getDefaultRootElement();
|
|
|
|
Element lastMessageElement = null;
|
|
|
|
for(int i = rootElement.getElementCount() - 1; i >= 0; i --)
|
|
{
|
|
String idAttr = (String) rootElement.getElement(i)
|
|
.getAttributes().getAttribute("identifier");
|
|
|
|
if (idAttr != null && idAttr.equals("messageHeader"))
|
|
{
|
|
lastMessageElement = rootElement.getElement(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(lastMessageElement == null)
|
|
return new Date(0);
|
|
|
|
String dateObject = lastMessageElement
|
|
.getAttributes().getAttribute("date").toString();
|
|
|
|
return new Date(Long.parseLong(dateObject));
|
|
}
|
|
|
|
/**
|
|
* Formats HTML tags <br/> to <br> or <BR/> to <BR>.
|
|
* The reason of this function is that the ChatPanel does not support
|
|
* <br /> closing tags (XHTML syntax), thus we have to remove every
|
|
* slash from each <br /> tags.
|
|
* @param message The source message string.
|
|
* @return The message string with properly formatted <br> tags.
|
|
*/
|
|
private String processBrTags(String message)
|
|
{
|
|
// The resulting message after being processed by this function.
|
|
StringBuffer processedMessage = new StringBuffer();
|
|
|
|
// Compile the regex to match something like <br .. /> or <BR .. />.
|
|
// This regex is case sensitive and keeps the style or other
|
|
// attributes of the <br> tag.
|
|
Matcher m
|
|
= Pattern.compile("<\\s*[bB][rR](.*?)(/\\s*>)").matcher(message);
|
|
int start = 0;
|
|
|
|
// while we find some <br /> closing tags with a slash inside.
|
|
while(m.find())
|
|
{
|
|
// First, we have to copy all the message preceding the <br> tag.
|
|
processedMessage.append(message.substring(start, m.start()));
|
|
// Then, we find the position of the slash inside the tag.
|
|
int slash_index = m.group().lastIndexOf("/");
|
|
// We copy the <br> tag till the slash exclude.
|
|
processedMessage.append(m.group().substring(0, slash_index));
|
|
// We copy all the end of the tag following the slash exclude.
|
|
processedMessage.append(m.group().substring(slash_index+1));
|
|
start = m.end();
|
|
}
|
|
// Finally, we have to add the end of the message following the last
|
|
// <br> tag, or the whole message if there is no <br> tag.
|
|
processedMessage.append(message.substring(start));
|
|
|
|
return processedMessage.toString();
|
|
}
|
|
|
|
/**
|
|
* Formats HTML tags <img ... /> to < img ... ></img> or
|
|
* <IMG ... /> to <IMG></IMG>.
|
|
* The reason of this function is that the ChatPanel does not support
|
|
* <img /> tags (XHTML syntax).
|
|
* Thus, we remove every slash from each <img /> and close it with a
|
|
* separate closing tag.
|
|
* @param message The source message string.
|
|
* @return The message string with properly formatted <img> tags.
|
|
*/
|
|
private String processImgTags(String message)
|
|
{
|
|
// The resulting message after being processed by this function.
|
|
StringBuffer processedMessage = new StringBuffer();
|
|
|
|
// Compile the regex to match something like <img ... /> or
|
|
// <IMG ... />. This regex is case sensitive and keeps the style,
|
|
// src or other attributes of the <img> tag.
|
|
Pattern p = Pattern.compile("<\\s*[iI][mM][gG](.*?)(/\\s*>)");
|
|
Matcher m = p.matcher(message);
|
|
int slash_index;
|
|
int start = 0;
|
|
|
|
// while we find some <img /> self-closing tags with a slash inside.
|
|
while(m.find()){
|
|
// First, we have to copy all the message preceding the <img> tag.
|
|
processedMessage.append(message.substring(start, m.start()));
|
|
// Then, we find the position of the slash inside the tag.
|
|
slash_index = m.group().lastIndexOf("/");
|
|
// We copy the <img> tag till the slash exclude.
|
|
processedMessage.append(m.group().substring(0, slash_index));
|
|
// We copy all the end of the tag following the slash exclude.
|
|
processedMessage.append(m.group().substring(slash_index+1));
|
|
// We close the tag with a separate closing tag.
|
|
processedMessage.append("</img>");
|
|
start = m.end();
|
|
}
|
|
// Finally, we have to add the end of the message following the last
|
|
// <img> tag, or the whole message if there is no <img> tag.
|
|
processedMessage.append(message.substring(start));
|
|
|
|
return processedMessage.toString();
|
|
}
|
|
|
|
/**
|
|
* Extend Editor pane to add URL tooltips.
|
|
*/
|
|
private class MyTextPane
|
|
extends JTextPane
|
|
{
|
|
/**
|
|
* Returns the string to be used as the tooltip for <i>event</i>.
|
|
*
|
|
* @param event the <tt>MouseEvent</tt>
|
|
* @return the string to be used as the tooltip for <i>event</i>.
|
|
*/
|
|
@Override
|
|
public String getToolTipText(MouseEvent event)
|
|
{
|
|
return
|
|
((currentHref != null) && (currentHref.length() != 0))
|
|
? currentHref
|
|
: null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a custom component at the end of the conversation.
|
|
*
|
|
* @param component the component to add at the end of the conversation.
|
|
*/
|
|
public void addComponent(ChatConversationComponent component)
|
|
{
|
|
synchronized (scrollToBottomRunnable)
|
|
{
|
|
StyleSheet styleSheet = document.getStyleSheet();
|
|
Style style
|
|
= styleSheet
|
|
.addStyle(
|
|
StyleConstants.ComponentElementName,
|
|
styleSheet.getStyle("body"));
|
|
|
|
// The image must first be wrapped in a style
|
|
style
|
|
.addAttribute(
|
|
AbstractDocument.ElementNameAttribute,
|
|
StyleConstants.ComponentElementName);
|
|
|
|
TransparentPanel wrapPanel
|
|
= new TransparentPanel(new BorderLayout());
|
|
|
|
wrapPanel.add(component, BorderLayout.NORTH);
|
|
|
|
style
|
|
.addAttribute(StyleConstants.ComponentAttribute, wrapPanel);
|
|
style.addAttribute("identifier", "messageHeader");
|
|
style.addAttribute("date", component.getDate().getTime());
|
|
|
|
scrollToBottomIsPending = true;
|
|
|
|
// Insert the component style at the end of the text
|
|
try
|
|
{
|
|
document
|
|
.insertString(document.getLength(), "ignored text", style);
|
|
}
|
|
catch (BadLocationException e)
|
|
{
|
|
logger.error("Insert in the HTMLDocument failed.", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers a new link click listener.
|
|
*
|
|
* @param listener the object that should be notified when an internal
|
|
* link was clicked.
|
|
*/
|
|
public void addChatLinkClickedListener(ChatLinkClickedListener listener)
|
|
{
|
|
if(!chatLinkClickedListeners.contains(listener))
|
|
chatLinkClickedListeners.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Remove a registered link click listener.
|
|
*
|
|
* @param listener a registered click listener to remove
|
|
*/
|
|
public void removeChatLinkClickedListener(ChatLinkClickedListener listener)
|
|
{
|
|
chatLinkClickedListeners.remove(listener);
|
|
}
|
|
|
|
/**
|
|
* Returns the date string to show for the given date.
|
|
*
|
|
* @param date the date to format
|
|
* @return the date string to show for the given date
|
|
*/
|
|
public static String getDateString(long date)
|
|
{
|
|
if (GuiUtils.compareDatesOnly(date, System.currentTimeMillis()) < 0)
|
|
{
|
|
StringBuffer dateStrBuf = new StringBuffer();
|
|
|
|
GuiUtils.formatDate(date, dateStrBuf);
|
|
dateStrBuf.append(" ");
|
|
return dateStrBuf.toString();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Reloads images.
|
|
*/
|
|
public void loadSkin()
|
|
{
|
|
openLinkItem.setIcon(
|
|
new ImageIcon(ImageLoader.getImage(ImageLoader.BROWSER_ICON)));
|
|
copyLinkItem.setIcon(
|
|
new ImageIcon(ImageLoader.getImage(ImageLoader.COPY_ICON)));
|
|
|
|
getRightButtonMenu().loadSkin();
|
|
}
|
|
|
|
/**
|
|
* Highlights the string in multi user chat.
|
|
*
|
|
* @param message the message to process
|
|
* @param contentType the content type of the message
|
|
* @param keyWord the keyword to highlight
|
|
* @return the message string with the keyword highlighted
|
|
*/
|
|
public String processChatRoomHighlight(String message, String contentType,
|
|
String keyWord)
|
|
{
|
|
return processKeyword(message, contentType, keyWord);
|
|
}
|
|
|
|
public String processMeCommand(ChatMessage chatMessage)
|
|
{
|
|
String contentType = chatMessage.getContentType();
|
|
String message = chatMessage.getMessage();
|
|
|
|
String msgID = "message";
|
|
String chatString = "";
|
|
String endHeaderTag = "";
|
|
|
|
String startDivTag = "<DIV identifier=\"" + msgID + "\">";
|
|
String endDivTag = "</DIV>";
|
|
|
|
String startPlainTextTag;
|
|
String endPlainTextTag;
|
|
|
|
if (HTML_CONTENT_TYPE.equals(contentType))
|
|
{
|
|
startPlainTextTag = "";
|
|
endPlainTextTag = "";
|
|
}
|
|
else
|
|
{
|
|
startPlainTextTag = START_PLAINTEXT_TAG;
|
|
endPlainTextTag = END_PLAINTEXT_TAG;
|
|
}
|
|
|
|
if (message.length() > 4 && message.substring(0, 4).equals("/me "))
|
|
{
|
|
chatString = startDivTag + "<B><I>";
|
|
|
|
endHeaderTag = "</I></B>" + endDivTag;
|
|
|
|
chatString +=
|
|
|
|
processHTMLChars("*** " + chatMessage.getContactName() + " "
|
|
+ message.substring(4))
|
|
+ endHeaderTag;
|
|
|
|
Map<String, ReplacementService> listSources =
|
|
GuiActivator.getReplacementSources();
|
|
|
|
Iterator<Entry<String, ReplacementService>> entrySetIter =
|
|
listSources.entrySet().iterator();
|
|
StringBuffer msgStore = new StringBuffer(chatString);
|
|
|
|
for (int i = 0; i < listSources.size(); i++)
|
|
{
|
|
Map.Entry<String, ReplacementService> entry =
|
|
entrySetIter.next();
|
|
|
|
ReplacementService source = entry.getValue();
|
|
|
|
boolean isSmiley = source instanceof SmiliesReplacementService;
|
|
if (isSmiley)
|
|
{
|
|
String sourcePattern = source.getPattern();
|
|
Pattern p =
|
|
Pattern.compile(sourcePattern, Pattern.CASE_INSENSITIVE
|
|
| Pattern.DOTALL);
|
|
Matcher m = p.matcher(msgStore);
|
|
|
|
StringBuffer msgTemp = new StringBuffer(chatString);
|
|
|
|
while (m.find())
|
|
{
|
|
msgTemp.insert(m.start(), startPlainTextTag);
|
|
msgTemp.insert(m.end() + startPlainTextTag.length(),
|
|
endPlainTextTag);
|
|
|
|
}
|
|
if (msgTemp.length() != msgStore.length())
|
|
msgStore = msgTemp;
|
|
}
|
|
}
|
|
|
|
return msgStore.toString();
|
|
}
|
|
else
|
|
return "";
|
|
}
|
|
|
|
private static String createIncomingMessageTag(
|
|
String messageID,
|
|
String incomingMessageHeader,
|
|
String incomingMessageParagraph)
|
|
{
|
|
StringBuffer messageBuff = new StringBuffer();
|
|
|
|
// <div class="box">
|
|
// <div class="topleft">
|
|
// <div class="topright">
|
|
// <div class="messageDiv">
|
|
// <h3>Header</h3>
|
|
// <p>Text</p>
|
|
// </div>
|
|
// </div>
|
|
// </div>
|
|
// <div class="bottomleft">
|
|
// <div class="bottomright">
|
|
// </div>
|
|
// </div>
|
|
// </div>
|
|
|
|
messageBuff.append("<div " + createBoxStyle() + ">");
|
|
messageBuff.append("<div " + createTopLeftStyle() + ">");
|
|
messageBuff.append("<div " + createTopRightStyle() + ">");
|
|
messageBuff.append("<div identifier=\"" + messageID
|
|
+ "\" " + createMessageDivStyle() + ">");
|
|
messageBuff.append(incomingMessageHeader);
|
|
messageBuff.append(incomingMessageParagraph);
|
|
messageBuff.append("</div>");
|
|
messageBuff.append("</div>");
|
|
messageBuff.append("</div>");
|
|
messageBuff.append("<div " + createBottomLeftStyle() + ">");
|
|
messageBuff.append("<div " + createBottomRightStyle() + ">");
|
|
messageBuff.append("</div>");
|
|
messageBuff.append("</div>");
|
|
messageBuff.append("</div>");
|
|
|
|
return messageBuff.toString();
|
|
}
|
|
|
|
private static String createOutgoingMessageStyle()
|
|
{
|
|
StringBuffer styleBuff = new StringBuffer();
|
|
|
|
styleBuff.append("background-image:");
|
|
styleBuff.append("url('bundle://30.0:1/resources/images/impl/gui/lookandfeel/selectedTabMiddle.png');");
|
|
styleBuff.append("background-repeat:");
|
|
styleBuff.append("repeat-x;");
|
|
|
|
return styleBuff.toString();
|
|
}
|
|
|
|
private static String createSmsMessageStyle()
|
|
{
|
|
StringBuffer styleBuff = new StringBuffer();
|
|
|
|
styleBuff.append("background-image:");
|
|
styleBuff.append("url('bundle://30.0:1/resources/images/impl/gui/lookandfeel/tabRight.png');");
|
|
styleBuff.append("background-repeat:");
|
|
styleBuff.append("repeat-x;");
|
|
|
|
return styleBuff.toString();
|
|
}
|
|
|
|
// .box {
|
|
// width: 100%;
|
|
// margin: 0px auto;
|
|
// }
|
|
private static String createBoxStyle()
|
|
{
|
|
return "style=\"width: 100%;"
|
|
+ " margin-top: 0px;"
|
|
+ " margin-bottom: 0px;"
|
|
+ " margin-left: auto;"
|
|
+ " margin-right: auto;\"";
|
|
}
|
|
|
|
// .box div.topleft {
|
|
// display: block;
|
|
// background: url("i/box-bg.png") top left no-repeat white;
|
|
// padding: 0em 0em 0em 1.0em;
|
|
// }
|
|
private static String createTopLeftStyle()
|
|
{
|
|
return "style=\"display: block;"
|
|
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
|
|
+ " background-repeat: no-repeat;"
|
|
+ " background-position: top left;"
|
|
+ " background-color: #FFFFFF;"
|
|
+ " padding-top: 0em;"
|
|
+ " padding-right: 0em;"
|
|
+ " padding-bottom: 0em;"
|
|
+ " padding-left: 0em;"
|
|
+ "\"";
|
|
}
|
|
|
|
// .box div.topright {
|
|
// display: block;
|
|
// background: url("i/box-bg.png") top right no-repeat white;
|
|
// padding: 1.0em;
|
|
// margin: -1.0em 0 0 1.0em;
|
|
// }
|
|
private static String createTopRightStyle()
|
|
{
|
|
return "style=\"display: block;"
|
|
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
|
|
+ " background-repeat: no-repeat;"
|
|
+ " background-position: top right;"
|
|
+ " background-color: #FFFFFF;"
|
|
+ " padding-top: 1em;"
|
|
+ " padding-right: 1em;"
|
|
+ " padding-bottom: 1em;"
|
|
+ " padding-left: 1em;"
|
|
+ " margin-top: -1.0em;"
|
|
+ " margin-right: 0em;"
|
|
+ " margin-bottom: 0em;"
|
|
+ " margin-left: 1.0em;"
|
|
+ "\"";
|
|
}
|
|
|
|
// .box div.bottomleft {
|
|
// display: block;
|
|
// height: 55px;
|
|
// margin-top: -1.0em;
|
|
// background: url("i/box-bg.png") bottom left no-repeat white;
|
|
// }
|
|
private static String createBottomLeftStyle()
|
|
{
|
|
return "style=\"display: block;"
|
|
+ " height: 25px;"
|
|
+ " margin-top: -1.0em;"
|
|
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
|
|
+ " background-repeat: no-repeat;"
|
|
+ " background-position: bottom left;"
|
|
+ " background-color: #FFFFFF;"
|
|
+ "\"";
|
|
}
|
|
|
|
// .box div.bottomright {
|
|
// display: block;
|
|
// background: url("i/box-bg.png") bottom right no-repeat white;
|
|
// height: 55px;
|
|
// margin-left: 3.0em;
|
|
// }
|
|
private static String createBottomRightStyle()
|
|
{
|
|
return "style=\"display: block;"
|
|
+ " height: 25px;"
|
|
+ " margin-left: 3.0em;"
|
|
+ " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
|
|
+ " background-repeat: no-repeat;"
|
|
+ " background-position: bottom right;"
|
|
+ " background-color: #FFFFFF;"
|
|
+ "\"";
|
|
}
|
|
|
|
// .box div.topright div {
|
|
// margin-right: 1.5em;
|
|
// }
|
|
private static String createMessageDivStyle()
|
|
{
|
|
return "style=\"margin-right: 1.5em;\"";
|
|
}
|
|
|
|
// .box h4 {
|
|
// margin-bottom: 0.4em;
|
|
// background-image: none;
|
|
// background-repeat: no-repeat;
|
|
// margin:0;
|
|
// padding:0;
|
|
// text-align:center;
|
|
// padding-bottom:15px;
|
|
// }
|
|
} |