package chatty.gui.components;
import chatty.Chatty;
import chatty.gui.MainGui;
import chatty.gui.UrlOpener;
import chatty.util.DateTime;
import chatty.util.JSONUtil;
import chatty.util.MiscUtil;
import chatty.util.UrlRequest;
import chatty.util.settings.Settings;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/**
* Show short announcements that are requested from a JSON file.
*
* When the "Mark as read" button is pressed, the timestamp of the latest news
* is stored to prevent those news from being shown as new.
*
* @author tduva
*/
public class NewsDialog extends JDialog {
private static final Logger LOGGER = Logger.getLogger(NewsDialog.class.getName());
private static final String NEWS_URL = "http://chatty.github.io/news.json";
private static final String NEWS_URL_TEST = "http://127.0.0.1/twitch/news.json";
private static final String SETTING_LAST_READ_TIMESTAMP = "newsLastRead";
private static final int REQUEST_DELAY = 60*1000;
private static final int TIMER_DELAY = (int)TimeUnit.HOURS.toMillis(6);
private final MainGui main;
private final Settings settings;
// State
private long lastRequested;
private long latestNewsTimestamp;
private String cachedNews;
// GUI
private final JTextPane news;
private final JButton refreshButton;
public NewsDialog(MainGui main, final Settings settings) {
super(main);
this.main = main;
this.settings = settings;
news = new JTextPane();
news.setEditable(false);
news.addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
String url = e.getURL().toString();
String protocol = e.getURL().getProtocol();
if (protocol.equals("http") || protocol.equals("https")) {
UrlOpener.openUrlPrompt(NewsDialog.this, url, true);
}
}
}
});
news.setContentType("text/html");
add(new JScrollPane(news), BorderLayout.CENTER);
JPanel buttons = new JPanel(new GridBagLayout());
add(buttons, BorderLayout.SOUTH);
final JButton markRead = new JButton("Mark as read & Close");
final JButton close = new JButton("Close");
refreshButton = new JButton(new ImageIcon(NewsDialog.class.getResource("view-refresh.png")));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.fill = GridBagConstraints.VERTICAL;
gbc.anchor = GridBagConstraints.WEST;
gbc.weightx = 0.8;
gbc.insets = new Insets(5, 4, 5, 4);
buttons.add(refreshButton, gbc);
gbc.gridx = 1;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.EAST;
gbc.weightx = 0;
gbc.insets = new Insets(5, 4, 5, 4);
buttons.add(markRead, gbc);
gbc.gridx = 2;
gbc.gridy = 0;
gbc.insets = new Insets(5, 1, 5, 4);
buttons.add(close, gbc);
ActionListener buttonAction = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == markRead) {
if (latestNewsTimestamp > 0) {
settings.setLong(SETTING_LAST_READ_TIMESTAMP, latestNewsTimestamp);
setNews(cachedNews);
}
setVisible(false);
} else if (e.getSource() == close) {
setVisible(false);
}
else if (e.getSource() == refreshButton) {
requestNews(false);
}
}
};
refreshButton.addActionListener(buttonAction);
markRead.addActionListener(buttonAction);
close.addActionListener(buttonAction);
pack();
setMinimumSize(new Dimension(400, 300));
setTitle("Announcements");
Timer timer = new Timer(TIMER_DELAY, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
autoRequestNews(false);
}
});
timer.setRepeats(true);
timer.start();
}
/**
* Shows the dialog centered on the main GUI and requests the latest news if
* the request delay has passed.
*/
public void showDialog() {
setLocationRelativeTo(main);
setVisible(true);
getNews(false);
}
/**
* Requests the news only if the auto request setting is enabled.
*
* @param openIfUnread Open the dialog automatically for new news
*/
public void autoRequestNews(boolean openIfUnread) {
if (settings.getBoolean("newsAutoRequest")) {
getNews(openIfUnread);
}
}
/**
* Requests the news from the server if the request delay has passed or
* load up a cached version.
*
* @param openIfUnread Open up the dialog automatically if new announcements
* are available
*/
private void getNews(boolean openIfUnread) {
if (System.currentTimeMillis() - lastRequested < REQUEST_DELAY) {
setNews(cachedNews);
return;
}
requestNews(openIfUnread);
}
/**
* Requests the announcements from the server.
*/
private void requestNews(final boolean openIfUnread) {
news.setText("Loading..");
latestNewsTimestamp = 0;
lastRequested = System.currentTimeMillis();
refreshButton.setEnabled(false);
UrlRequest request = new UrlRequest(Chatty.DEBUG ? NEWS_URL_TEST : NEWS_URL) {
@Override
public void requestResult(final String result, final int responseCode) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (responseCode == 200) {
int unread = setNews(result);
if (openIfUnread && unread > 0) {
showDialog();
}
} else {
news.setText("Error loading news. ("+responseCode+")");
}
refreshButton.setEnabled(true);
}
});
}
};
new Thread(request).start();
}
/**
* Parses the given data and fills the dialog accordingly. This may be
* either directly from the server or a cached version. Updates the title
* and main menu notification accordingly.
*
* @param data The JSON containing the announcements
* @return The number of new announcements
*/
private int setNews(String data) {
if (data == null || data.isEmpty()) {
return 0;
}
try {
int newCount = parseNews(data);
cachedNews = data;
if (newCount > 0) {
setTitle(String.format("Announcements (%d new)", newCount));
main.setAnnouncementAvailable(true);
} else {
setTitle("Announcements");
main.setAnnouncementAvailable(false);
}
return newCount;
} catch (Exception ex) {
news.setText("Error loading news.");
LOGGER.warning(MiscUtil.getStackTrace(ex));
}
return 0;
}
private int parseNews(String text) throws ParseException {
// Data
long lastRead = settings.getLong(SETTING_LAST_READ_TIMESTAMP);
int unreadCount = 0;
JSONParser parser = new JSONParser();
JSONObject data = (JSONObject)parser.parse(text);
JSONArray list = (JSONArray)data.get("news");
// HTML
final SimpleDateFormat DATETIME = new SimpleDateFormat("yyyy-MM-dd HH:mm");
StringBuilder sb = new StringBuilder("<html><head><style>"
+ "body { font:sans-serif; font-size: 12pt; margin: 3px; }"
+ "h2 { margin: 12px 0 0 0; font-size: 16pt; }"
+ ".time { font-size: 12pt; color: #333333; font-weight: bold; }"
+ ".new { background-color: yellow; font-size: 12pt; color: #777777; }"
+ ".old { color: #777777; }"
+ "</style></head><body>"
+ "<p style='margin-top:0;background-color:#EEEEEE;padding:3px;'>"
+ ""+parseContent((String)data.get("intro"))
+ "</p>");
for (Object o : list) {
// Data
JSONObject entry = (JSONObject)o;
String title = (String)entry.get("title");
String content = (String)entry.get("content");
long time_added = ((Number)entry.get("timestamp")).longValue()*1000;
boolean old = JSONUtil.getBoolean(entry, "old", false);
if (time_added > lastRead && !old) {
unreadCount++;
}
if (time_added > latestNewsTimestamp) {
latestNewsTimestamp = time_added;
}
// HTML
sb.append("<h2>").append(title);
if (old) {
sb.append(" <span class='old'>(old)</span>");
}
else if (time_added > lastRead) {
sb.append(" <span class='new'>(new)</span>");
}
sb.append("</h2>");
sb.append(String.format(" <div class=\"time\">%s (%s)</div><p>%s</p>",
DATETIME.format(new Date(time_added)),
DateTime.agoText(time_added),
parseContent(content)));
}
sb.append("</body></html>");
// Replace text in dialog
news.setDocument(news.getEditorKit().createDefaultDocument());
news.setText(sb.toString());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
news.scrollRectToVisible(new Rectangle(0, 0, 1, 1));
}
});
return unreadCount;
}
private String parseContent(String content) {
return content.replaceAll("\\[([^]]+)\\]\\(([^)]+)\\)", "<a href=\"$2\">$1</a>");
}
}