/* * Kontalk Java client * Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.view; import javax.net.ssl.SSLHandshakeException; import javax.swing.Action; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.KeyStroke; import javax.swing.text.DefaultEditorKit; import java.awt.Component; import java.awt.Desktop; import java.awt.Font; import java.awt.Image; import java.awt.Toolkit; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.security.cert.CertificateException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.alee.extended.filechooser.WebFileChooserField; import com.alee.laf.menu.WebPopupMenu; import com.alee.laf.optionpane.WebOptionPane; import com.alee.laf.text.WebTextArea; import com.alee.utils.filefilter.ImageFilesFilter; import org.apache.commons.lang.StringUtils; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.sasl.SASLErrorException; import org.jxmpp.util.XmppStringUtils; import org.kontalk.misc.JID; import org.kontalk.misc.KonException; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.Member; import org.kontalk.model.chat.SingleChat; import org.kontalk.persistence.Config; import org.kontalk.system.AttachmentManager; import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; import org.ocpsoft.prettytime.PrettyTime; /** * Various utilities used in view. * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>} */ final class Utils { private static final Logger LOGGER = Logger.getLogger(Utils.class.getName()); static final String IMG_DIR = "img"; static final DateFormat SHORT_DATE_FORMAT = new SimpleDateFormat("HH:mm"); static final DateFormat MID_DATE_FORMAT = new SimpleDateFormat("EEE, d MMM HH:mm"); static final DateFormat LONG_DATE_FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss"); static final PrettyTime PRETTY_TIME = new PrettyTime(); private static final DateFormat DAY_DATE_FORMAT = new SimpleDateFormat("EEE, d MMMM"); private static final DateFormat DAY_YEAR_DATE_FORMAT = new SimpleDateFormat("EEE, d MMMM yyyy"); private Utils() {} /* fields */ static WebFileChooserField createImageChooser(String path) { WebFileChooserField chooser = new WebFileChooserField(); if (!path.isEmpty()) chooser.setSelectedFile(new File(path)); chooser.setMultiSelectionEnabled(false); chooser.setShowRemoveButton(true); chooser.getWebFileChooser().setFileFilter(new ImageFilesFilter()); File file = new File(path); if (file.exists()) { chooser.setSelectedFile(file); } if (file.getParentFile() != null && file.getParentFile().exists()) chooser.getWebFileChooser().setCurrentDirectory(file.getParentFile()); return chooser; } static WebTextArea createFingerprintArea() { WebTextArea area = new WebTextArea(); area.setEditable(false); area.setOpaque(false); area.setFontName(Font.DIALOG); area.setFontSizeAndStyle(13, true, false); return area; } static Runnable createLinkRunnable(final Path path) { return new Runnable () { @Override public void run () { File file = path.toFile(); if (!file.exists()) { LOGGER.info("file does not exist: " + file); return; } Desktop dt = Desktop.getDesktop(); try { dt.open(file); } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't open path", ex); } } }; } // NOTE: use only with text components static WebPopupMenu createCopyMenu(boolean modifiable) { WebPopupMenu menu = new WebPopupMenu(); if (modifiable) { Action cut = new DefaultEditorKit.CutAction(); cut.putValue(Action.NAME, Tr.tr("Cut")); cut.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control X")); menu.add(cut); } Action copy = new DefaultEditorKit.CopyAction(); copy.putValue(Action.NAME, Tr.tr("Copy")); copy.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control C")); menu.add(copy); if (modifiable) { Action paste = new DefaultEditorKit.PasteAction(); paste.putValue(Action.NAME, Tr.tr("Paste")); paste.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control V")); menu.add(paste); } return menu; } /* images */ static Icon getIcon(String fileName) { return new ImageIcon(getImage(fileName)); } static Image getImage(String fileName) { URL imageUrl = ClassLoader.getSystemResource(Paths.get(IMG_DIR, fileName).toString()); if (imageUrl == null) { LOGGER.warning("can't find icon image resource"); return new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB); } return Toolkit.getDefaultToolkit().createImage(imageUrl); } /* strings */ static String name(Contact contact, int maxLength) { String name = name_(contact, maxLength); return !name.isEmpty() ? name : "("+Tr.tr("Unknown")+")"; } static String displayName(Contact contact) { return displayName(contact, Integer.MAX_VALUE); } static String displayName(Contact contact, int maxLength) { return displayName(contact, contact.getJID(), maxLength); } static String displayName(Contact contact, JID jid, int maxLength) { String name = name_(contact, maxLength); return !name.isEmpty() ? name : jid(jid, maxLength); } static String displayNames(List<JID> jids, ContactList contactList, int maxJIDLength) { List<String> nameList = new ArrayList<>(jids.size()); for (JID jid : jids) { Contact contact = contactList.get(jid).orElse(null); nameList.add(contact != null ? displayName(contact, jid, Integer.MAX_VALUE) : jid(jid, maxJIDLength)); } return StringUtils.join(nameList, ", "); } private static String displayNames(List<Contact> contacts) { return displayNames(contacts, Integer.MAX_VALUE); } static String displayNames(List<Contact> contacts, int maxLength) { List<String> nameList = new ArrayList<>(contacts.size()); for (Contact contact : contacts) { nameList.add(displayName(contact, maxLength)); } return StringUtils.join(nameList, ", "); } private static String name_(Contact contact, int maxLength) { return contact.isDeleted() ? "("+Tr.tr("Deleted")+")" : contact.isMe() ? Tr.tr("You") : StringUtils.abbreviate(contact.getName(), maxLength); } static String jid(JID jid, int maxLength) { String local = jid.local(), domain = jid.domain(); if (jid.isHash()) local = "[" + local.substring(0, Math.min(local.length(), 6)) + "]"; local = StringUtils.abbreviate(local, (int) ((maxLength-1) * 0.4)); domain = StringUtils.abbreviate(domain, (int) ((maxLength-1) * 0.6)); return XmppStringUtils.completeJidFrom(local, domain); } static String chatTitle(Chat chat) { if (chat.isGroupChat()) { String subj = chat.getSubject(); return !subj.isEmpty() ? subj : Tr.tr("Group Chat"); } else { return Utils.displayNames(chat.getAllContacts()); } } static String chatTooltip(Chat chat) { if (chat instanceof SingleChat) return jid(((SingleChat) chat).getMember().getContact().getJID(), View.MAX_JID_LENGTH); int numMembers = chat.getAllMembers().size(); return numMembers == 1 ? Tr.tr("one member") : String.format(Tr.tr("%1$s members"), numMembers); } static String fingerprint(String fp) { fp = fp.toUpperCase(); int m = fp.length() / 2; return group(fp.substring(0, m)) + "\n" + group(fp.substring(m)); } private static String group(String s) { return StringUtils.join(s.split("(?<=\\G.{" + 4 + "})"), " "); } static String role(Member.Role role) { switch (role) { case OWNER : return "[" + Tr.tr("Group Owner") + "]"; default: return ""; } } static String mainStatus(Contact c, boolean withLabel) { Contact.Subscription subStatus = c.getSubScription(); return c.isMe() ? Tr.tr("Myself") : c.isBlocked() ? Tr.tr("Blocked") : c.getOnline() == Contact.Online.YES ? Tr.tr("Online") : c.getOnline() == Contact.Online.ERROR ? Tr.tr("Not reachable") : subStatus == Contact.Subscription.UNSUBSCRIBED ? Tr.tr("Not authorized") : subStatus == Contact.Subscription.PENDING ? Tr.tr("Waiting for authorization") : lastSeen(c, withLabel, true); } static String lastSeen(Contact contact, boolean withLabel, boolean pretty) { Date d = contact.getLastSeen().orElse(null); return d == null ? (withLabel ? Tr.tr("Not seen yet") : "") : (withLabel ? Tr.tr("Last seen:") + " " : "") + (pretty ? Utils.PRETTY_TIME.format(d) : Utils.MID_DATE_FORMAT.format(d)); } static String getErrorText(KonException ex) { String eol = " " + EncodingUtils.EOL; String errorText; switch (ex.getError()) { case IMPORT_ARCHIVE: errorText = Tr.tr("Can't open key archive."); break; case IMPORT_READ_FILE: errorText = Tr.tr("Can't load all keyfiles from archive."); break; case IMPORT_KEY: errorText = Tr.tr("Can't create personal key from key files.") + " "; if (ex.getCauseClass().equals(IOException.class)) { errorText += eol + Tr.tr("Is the public key file valid?"); } if (ex.getCauseClass().equals(CertificateException.class)) { errorText += eol + Tr.tr("Are all key files valid?"); } break; case CHANGE_PASS: errorText = Tr.tr("Can't change password. Internal error(!?)"); break; case WRITE_FILE: errorText = Tr.tr("Can't write key files to configuration directory."); break; case READ_FILE: case LOAD_KEY: errorText = ""; switch (ex.getError()) { case READ_FILE: errorText = Tr.tr("Can't read key files from configuration directory."); break; case LOAD_KEY: errorText = Tr.tr("Can't load key files from configuration directory."); break; } errorText += eol + Tr.tr("Please reimport your personal key."); break; case LOAD_KEY_DECRYPT: errorText = Tr.tr("Can't decrypt key. Is the passphrase correct?"); break; case CLIENT_CONNECT: errorText = Tr.tr("Can't connect to server."); if (ex.getCauseClass().equals(SmackException.ConnectionException.class)) { errorText += eol + Tr.tr("Is the server address correct?"); } else if (ex.getCauseClass().equals(SSLHandshakeException.class)) { errorText += eol + Tr.tr("The server rejects the personal key."); } else if (ex.getCauseClass().equals(SmackException.NoResponseException.class)) { errorText += eol + Tr.tr("The server does not respond."); } else { Throwable cause = ex.getCause(); if (cause != null) { Throwable causeCause = cause.getCause(); if (causeCause != null && causeCause.getClass().equals(SSLHandshakeException.class)) { errorText += eol + Tr.tr("The server certificate could not be validated."); } } } break; case CLIENT_LOGIN: errorText = Tr.tr("Can't login to server."); if (ex.getCauseClass().equals(SASLErrorException.class)) { errorText += eol + Tr.tr("The server rejects the account. Is the specified server correct and the account valid?"); } break; case CLIENT_ERROR: errorText = Tr.tr("Connection to server closed on error."); // TODO more details break; case DOWNLOAD_CREATE: case DOWNLOAD_EXECUTE: case DOWNLOAD_RESPONSE: case DOWNLOAD_WRITE: errorText = Tr.tr("Downloading file failed"); // TODO more details break; case UPLOAD_CREATE: case UPLOAD_EXECUTE: case UPLOAD_RESPONSE: errorText = Tr.tr("Uploading file failed"); // TODO more details break; default: errorText = Tr.tr("Unusual error:")+" "+ex.getError(); } return errorText; } /* misc */ static boolean confirmDeletion(Component parent, String text) { int selectedOption = WebOptionPane.showConfirmDialog(parent, text, Tr.tr("Please Confirm"), WebOptionPane.OK_CANCEL_OPTION, WebOptionPane.WARNING_MESSAGE); return selectedOption == WebOptionPane.OK_OPTION; } static Set<Contact> allContacts(ContactList contactList, boolean blocked) { boolean showMe = Config.getInstance().getBoolean(Config.VIEW_USER_CONTACT); return contactList.getAll(showMe, blocked); } static List<Contact> contactList(Chat chat) { List<Contact> contacts = new ArrayList<>(chat.getAllContacts()); contacts.sort(Utils::compareContacts); return contacts; } static List<Member> memberList(Chat chat) { List<Member> members = new ArrayList<>(chat.getAllMembers()); members.sort(new Comparator<Member>() { @Override public int compare(Member m1, Member m2) { return Utils.compareContacts(m1.getContact(), m2.getContact()); } }); return members; } static int compareContacts(Contact c1, Contact c2) { if (c1.isMe()) return +1; if (c2.isMe()) return -1; String s1 = StringUtils.defaultIfEmpty(c1.getName(), c1.getJID().asUnescapedString()); String s2 = StringUtils.defaultIfEmpty(c2.getName(), c2.getJID().asUnescapedString()); return s1.compareToIgnoreCase(s2); } static String getDateSeparatorText(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); boolean sameYear = cal.get(Calendar.YEAR) == Calendar.getInstance().get(Calendar.YEAR); DateFormat format = sameYear ? DAY_DATE_FORMAT : DAY_YEAR_DATE_FORMAT; return format.format(date); } static boolean isAllowedAttachmentFile(File file) { return file.length() <= AttachmentManager.MAX_ATT_SIZE; } }