package org.limewire.ui.swing.friends.chat; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Panel; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.HashSet; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.JEditorPane; import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.StyleSheet; import org.jdesktop.application.Application; import org.jdesktop.application.Resource; import org.jdesktop.swingx.JXButton; import org.jdesktop.swingx.painter.AbstractPainter; import org.limewire.friend.api.FriendConnectionEvent; import org.limewire.friend.api.MessageWriter; import org.limewire.friend.api.Network; import org.limewire.inject.LazySingleton; import org.limewire.listener.EventListener; import org.limewire.listener.ListenerSupport; import org.limewire.listener.SwingEDTEvent; import org.limewire.ui.swing.components.HTMLLabel; import org.limewire.ui.swing.components.OverlayPopupPanel; import org.limewire.ui.swing.mainframe.GlobalLayeredPane; import org.limewire.ui.swing.tray.Notification; import org.limewire.ui.swing.tray.TrayNotifier; import org.limewire.ui.swing.util.GuiUtils; import org.limewire.ui.swing.util.I18n; import static org.limewire.ui.swing.util.I18n.tr; import org.limewire.ui.swing.util.PainterUtils; import org.limewire.ui.swing.util.NativeLaunchUtils; import org.limewire.ui.swing.util.ResizeUtils; import com.google.inject.Inject; import com.google.inject.Provider; import net.miginfocom.swing.MigLayout; /** * Mediator for the chat window and chat button in the status bar. Listens for * sign on/ sign off events and updates the chat button. Listens for incoming messages * when signed on and lazily creates the chat frame only when needed. */ @LazySingleton public class ChatMediator { @Resource private Font font; @Resource private Color foreground; @Resource private Icon unviewedChatIcon; @Resource private Icon normalChatIcon; private final Provider<ChatHeader> chatHeaderProvider; private final JLayeredPane layeredPane; private final Provider<ChatModel> chatModel; private final Provider<ChatFrame> chatFrameProvider; private ChatFrame chatFrame; private Frame panel; private final JXButton chatButton; private final TrayNotifier trayNotifier; private IncomingListener incomingChatListener; private Set<String> unseenMessages = new HashSet<String>(); private volatile FriendConnectionEvent lastEvent; @Inject public ChatMediator(Provider<ChatFrame> chatFrameProvider, Provider<ChatHeader> chatHeaderProvider, TrayNotifier trayNotifier, Provider<ChatModel> chatModel, @GlobalLayeredPane JLayeredPane layeredPane) { this.chatFrameProvider = chatFrameProvider; this.chatHeaderProvider = chatHeaderProvider; this.layeredPane = layeredPane; this.trayNotifier = trayNotifier; this.chatModel = chatModel; chatButton = new JXButton(); initChatButton(); } /** * Returns the ChatFrame. This is lazily created on the first call. */ private Panel getChatFrame() { if(panel == null) { panel = new Frame(layeredPane); JPanel child; if(isFacebook()) { // LWC-4069 child = getFacebookPanel(); } else { chatFrame = chatFrameProvider.get(); child = chatFrame; } panel.add(child, BorderLayout.CENTER); child.revalidate(); } return panel; } private boolean isFacebook() { return lastEvent.getSource().getConfiguration().getType() == Network.Type.FACEBOOK; } /** * Returns the chat button displayed in the status panel. */ public JXButton getChatButton() { return chatButton; } /** * Sets the visibility of the ChatFrame. */ public void setVisible(boolean value) { getChatFrame().setVisible(value); panel.resize(); if(unseenMessages.size() > 0) { unseenMessages.clear(); setUnseenMessageCount(unseenMessages.size()); } getChatButton().repaint(); } /** * Returns true if the ChatFrame is visible, false otherwise. */ public boolean isVisible() { return panel != null && panel.isVisible(); } /** * Selects this friend's conversation if one already exists, or * starts a new conversation with this friend and selects it. */ public void startOrSelectConversation(String friendId) { setVisible(true); if(!isFacebook()) { // LWC-4069 chatFrame.selectOrStartConversation(chatModel.get().getChatFriend(friendId)); } } /** * Initializes the chat button. */ private void initChatButton() { GuiUtils.assignResources(this); chatButton.setFocusPainted(false); chatButton.setOpaque(false); chatButton.setBorder(null); chatButton.setContentAreaFilled(false); chatButton.setFocusable(false); chatButton.setBorder(BorderFactory.createEmptyBorder(2, 10, 0, 10)); chatButton.setPaintBorderInsets(true); chatButton.setFont(font); chatButton.setForeground(foreground); chatButton.setVisible(false); chatButton.setText(I18n.tr("Chat")); chatButton.setIcon(normalChatIcon); chatButton.setBackgroundPainter(new ChatButtonPainter()); } @Inject void register(ListenerSupport<FriendConnectionEvent> connectionSupport, final ListenerSupport<ChatMessageEvent> messageList) { final EventListener<ChatMessageEvent> messageListener = new EventListener<ChatMessageEvent>() { @Override @SwingEDTEvent public void handleEvent(ChatMessageEvent event) { handleChatMessage(event.getData()); } }; // listen for login/logout events connectionSupport.addListener(new EventListener<FriendConnectionEvent>() { @Override @SwingEDTEvent public void handleEvent(FriendConnectionEvent event) { lastEvent = event; switch(event.getType()) { // register listeners for incoming events with friends, make the // chat button visible case CONNECTED: chatModel.get().registerListeners(); if(incomingChatListener == null) { incomingChatListener = new IncomingListener() { @Override public void incomingChat(ChatFriend chatFriend, MessageWriter messageWriter) { getChatFrame(); chatFrame.startConversation(chatFriend, messageWriter); } }; } chatModel.get().addIncomingListener(incomingChatListener); getChatButton().setVisible(true); messageList.addListener(messageListener); break; // unregister listeners and hide the chat window/chat button case DISCONNECTED: getChatButton().setVisible(false); if(panel != null) { setVisible(false); if (chatFrame != null) { chatFrame.closeAllChats(); } panel.dispose(); } chatModel.get().unregisterListeners(); chatModel.get().removeIncomingListener(incomingChatListener); messageList.removeListener(messageListener); panel = null; break; } } }); // listen for mouse clicks on the chat button to show/hide window chatButton.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { setVisible(!isVisible()); } }); } /** * Listen for incoming messages. This doesn't care what the message is, it simply * updates UI components that a new message has arrived. * * @param message chat message */ public void handleChatMessage(Message message) { if (message.getType() != Message.Type.SENT) { String messageFriendID = message.getFriendID(); ChatFriend chatFriend = chatModel.get().getChatFriend(messageFriendID); // if the chat frame not visible, update unseen message if(!isVisible()) { chatFriend.setHasUnviewedMessages(true); unseenMessages.add(messageFriendID); setUnseenMessageCount(unseenMessages.size()); } // otherwise, if chatframe visible and the friend is not selected, update friend with unseen message. else if(chatFriend != chatFrame.getSelectedConversation() && chatFrame.getSelectedConversation() != null) { chatFriend.setHasUnviewedMessages(true); } } // if chat panel not visible, notify in tray if (message.getType() != Message.Type.SENT && (!GuiUtils.getMainFrame().isActive() || !isVisible())) { trayNotifier.showMessage(getNoticeForMessage(message)); } } /** * Creates Notification to display in the TrayNotifier. */ private Notification getNoticeForMessage(final Message message) { // todo: each message type should know how to display itself as a notification String title = message.getType() == Message.Type.SERVER ? tr("Message from the chat server") : tr("Chat from {0}", message.getSenderName()); Notification notification = new Notification(title, message.toString(), new AbstractAction(I18n.tr("Reply")) { @Override public void actionPerformed(ActionEvent e) { ActionMap map = Application.getInstance().getContext().getActionManager().getActionMap(); map.get("restoreView").actionPerformed(e); if(message.getType() != Message.Type.SERVER && message.getFriendID() != null) startOrSelectConversation(message.getFriendID()); else setVisible(true); } }); return notification; } /** * Updates the text for the chat button. */ private void setUnseenMessageCount(int count) { chatButton.setText(count > 0 ? I18n.tr("Chat ({0})", count) : I18n.tr("Chat")); chatButton.setIcon(count > 0 ? unviewedChatIcon : normalChatIcon); } public JPanel getFacebookPanel() { // LWC-4069 JPanel panel = new JPanel(new MigLayout("gap 10! 10!")); panel.setBorder(BorderFactory.createMatteBorder(1,1,0,1, Color.BLACK)); panel.add(chatHeaderProvider.get().getComponent(), "dock north"); JEditorPane editorPane = new JEditorPane(); editorPane.setContentType("text/html"); editorPane.setEditable(false); editorPane.setCaretPosition(0); editorPane.setSelectionColor(HTMLLabel.TRANSPARENT_COLOR); editorPane.setOpaque(false); editorPane.setFocusable(false); editorPane.setText("<HTML>" + ChatSettings.FACEBOOK_CHAT_DISABLED_TEXT.get() + "</HTML>"); StyleSheet mainStyle = ((HTMLDocument)editorPane.getDocument()).getStyleSheet(); String rules = "h1 { font-family: dialog; color: #313131; font-size: 12; font-weight: bold}" + "p {font-family: dialog; color: #313131; font-size: 11; }" ; StyleSheet newStyle = new StyleSheet(); newStyle.addRule(rules); mainStyle.addStyleSheet(newStyle); editorPane.addHyperlinkListener(new HyperlinkListener() { @Override public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { NativeLaunchUtils.openURL("http://www.facebook.com"); } } }); panel.add(editorPane); return panel; } /** * Heavy weight component so it displays over the browser. */ private class Frame extends OverlayPopupPanel { public Frame(JLayeredPane layeredPane) { super(layeredPane); setLayout(new BorderLayout()); ResizeUtils.forceSize(this, new Dimension(400, 240)); setVisible(false); resize(); } @Override public void resize() { Rectangle parentBounds = layeredPane.getBounds(); int w = getPreferredSize().width; int h = getPreferredSize().height; setLocation(parentBounds.width - w, parentBounds.height - h); } } private class ChatButtonPainter extends AbstractPainter<JXButton> { @Resource private Color rolloverBackground = PainterUtils.TRANSPARENT; @Resource private Color activeBackground = PainterUtils.TRANSPARENT; @Resource private Color activeBorder = PainterUtils.TRANSPARENT; @Resource private Color border = PainterUtils.TRANSPARENT; public ChatButtonPainter() { GuiUtils.assignResources(this); setCacheable(false); setAntialiasing(true); } @Override protected void doPaint(Graphics2D g, JXButton object, int width, int height) { if (panel != null && panel.isVisible()) { g.setPaint(activeBackground); g.fillRect(0, 0, width, height); g.setPaint(border); g.drawLine(0, 0, 0, height-1); g.drawLine(0, height-1, width-1, height-1); g.drawLine(width-1, 0, width-1, height-1); if (chatFrame != null && chatFrame.getSelectedConversation() != null) { g.setPaint(activeBorder); g.drawLine(0,0,width-2,0); } } else if (object.getModel().isRollover()) { g.setPaint(rolloverBackground); g.fillRect(0, 2, width-1, height-2); g.setPaint(activeBorder); g.drawLine(0, 1, 0, height-1); } } } }