diff --git a/core/src/main/groovy/com/muwire/core/Core.groovy b/core/src/main/groovy/com/muwire/core/Core.groovy index e6da95ca..e2417200 100644 --- a/core/src/main/groovy/com/muwire/core/Core.groovy +++ b/core/src/main/groovy/com/muwire/core/Core.groovy @@ -430,7 +430,8 @@ public class Core { eventBus.register(UIFeedUpdateEvent.class, feedClient) log.info "initializing results sender" - ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer, collectionManager) + ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, { getMyProfile()?.getHeader() } as Supplier, + props, certificateManager, chatServer, collectionManager) log.info "initializing search manager" SearchManager searchManager = new SearchManager(eventBus, me, resultsSender, props) diff --git a/core/src/main/groovy/com/muwire/core/search/ResultsSender.groovy b/core/src/main/groovy/com/muwire/core/search/ResultsSender.groovy index d915e639..c66edf96 100644 --- a/core/src/main/groovy/com/muwire/core/search/ResultsSender.groovy +++ b/core/src/main/groovy/com/muwire/core/search/ResultsSender.groovy @@ -7,6 +7,7 @@ import com.muwire.core.connection.Endpoint import com.muwire.core.connection.I2PConnector import com.muwire.core.filecert.CertificateManager import com.muwire.core.files.FileHasher +import com.muwire.core.profile.MWProfileHeader import com.muwire.core.util.DataUtil import com.muwire.core.Persona @@ -16,6 +17,7 @@ import java.util.concurrent.Executor import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Supplier import java.util.logging.Level import java.util.stream.Collectors import java.util.zip.GZIPOutputStream @@ -48,17 +50,20 @@ class ResultsSender { private final I2PConnector connector private final Persona me + private final Supplier myProfileHeader private final EventBus eventBus private final MuWireSettings settings private final CertificateManager certificateManager private final ChatServer chatServer private final CollectionManager collectionManager - ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings, - CertificateManager certificateManager, ChatServer chatServer, CollectionManager collectionManager) { + ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, Supplier myProfileHeader, + MuWireSettings settings, CertificateManager certificateManager, ChatServer chatServer, + CollectionManager collectionManager) { this.connector = connector; this.eventBus = eventBus this.me = me + this.myProfileHeader = myProfileHeader this.settings = settings this.certificateManager = certificateManager this.chatServer = chatServer @@ -109,7 +114,8 @@ class ResultsSender { messages : settings.allowMessages, feed : settings.fileFeed && settings.advertiseFeed, collections : collections, - path: path + path: path, + profileHeader: myProfileHeader.get() ) uiResultEvents << uiResultEvent } @@ -168,6 +174,9 @@ class ResultsSender { os.write("Feed: $feed\r\n".getBytes(StandardCharsets.US_ASCII)) boolean messages = settings.allowMessages os.write("Messages: $messages\r\n".getBytes(StandardCharsets.US_ASCII)) + MWProfileHeader header = myProfileHeader.get() + if (header != null) + os.write("ProfileHeader: ${myProfileHeader.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("\r\n".getBytes(StandardCharsets.US_ASCII)) dos = new DataOutputStream(new GZIPOutputStream(os)) results.each { diff --git a/core/src/main/groovy/com/muwire/core/search/UIResultEvent.groovy b/core/src/main/groovy/com/muwire/core/search/UIResultEvent.groovy index b02dd2eb..519edae8 100644 --- a/core/src/main/groovy/com/muwire/core/search/UIResultEvent.groovy +++ b/core/src/main/groovy/com/muwire/core/search/UIResultEvent.groovy @@ -3,7 +3,7 @@ package com.muwire.core.search import com.muwire.core.Event import com.muwire.core.InfoHash import com.muwire.core.Persona - +import com.muwire.core.profile.MWProfileHeader import net.i2p.data.Destination class UIResultEvent extends Event { @@ -23,6 +23,7 @@ class UIResultEvent extends Event { boolean messages Set collections String[] path + MWProfileHeader profileHeader private String fullPath diff --git a/gui/griffon-app/i18n/messages.properties b/gui/griffon-app/i18n/messages.properties index bbcd8b45..6c53fabd 100644 --- a/gui/griffon-app/i18n/messages.properties +++ b/gui/griffon-app/i18n/messages.properties @@ -270,6 +270,8 @@ FEED=Feed NEUTRAL=Neutral DISTRUST=Distrust COPY_FULL_ID=Copy Full ID +NO_PROFILE=This user does not have a profile + # results table (group by sender) DIRECT_SOURCES=Direct Sources diff --git a/gui/griffon-app/lifecycle/Initialize.groovy b/gui/griffon-app/lifecycle/Initialize.groovy index 25bc08bb..91aa59df 100644 --- a/gui/griffon-app/lifecycle/Initialize.groovy +++ b/gui/griffon-app/lifecycle/Initialize.groovy @@ -105,7 +105,7 @@ class Initialize extends AbstractLifecycleHandler { } else { fontSize = uiSettings.fontSize } - rowHeight = fontSize + 3 + rowHeight = Math.max(24, fontSize + 3) FontUIResource font = new FontUIResource(fontName, uiSettings.fontStyle, fontSize) def keys = lnf.getDefaults().keys() diff --git a/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy b/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy index ce537705..bcce2343 100644 --- a/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy @@ -1,6 +1,10 @@ package com.muwire.gui import com.muwire.core.InfoHash +import com.muwire.core.profile.MWProfileHeader +import com.muwire.gui.profile.ImageScaler +import com.muwire.gui.profile.PersonaOrProfile +import com.muwire.gui.profile.ThumbnailIcon import javax.annotation.Nonnull @@ -13,8 +17,11 @@ import griffon.inject.MVCMember import griffon.transform.Observable import griffon.metadata.ArtifactProviderFor +import javax.imageio.ImageIO +import javax.swing.Icon import javax.swing.SwingWorker import javax.swing.tree.DefaultMutableTreeNode +import java.awt.image.BufferedImage import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicReference @@ -131,7 +138,7 @@ class SearchTabModel { SenderBucket senderBucket = sendersBucket.get(event.sender) if (senderBucket == null) { - senderBucket = new SenderBucket(event.sender, senders.size()) + senderBucket = new SenderBucket(event.sender, event.profileHeader, senders.size()) sendersBucket[event.sender] = senderBucket senders << senderBucket } @@ -245,15 +252,40 @@ class SearchTabModel { } } - static class SenderBucket { + static class SenderBucket implements PersonaOrProfile { final Persona sender + private final Icon avatar + private final String profileTitle private final int rowIdx final List results = [] private int lastUpdatedIdx - SenderBucket(Persona sender, int rowIdx) { + SenderBucket(Persona sender, MWProfileHeader profileHeader,int rowIdx) { this.sender = sender this.rowIdx = rowIdx + if (profileHeader != null) { + Icon icon = null + try { + icon = new ThumbnailIcon(profileHeader.getThumbNail()) + } catch (IOException iox) {} + avatar = icon + profileTitle = profileHeader.getTitle() + } else { + avatar = null + profileTitle = null + } + } + + Persona getPersona() { + sender + } + + Icon getThumbnail() { + avatar + } + + String getTitle() { + profileTitle } List getPendingResults() { diff --git a/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy b/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy index 932ac84e..72f74734 100644 --- a/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy @@ -2,6 +2,9 @@ package com.muwire.gui import com.muwire.core.SharedFile import com.muwire.gui.SearchTabModel.SenderBucket +import com.muwire.gui.profile.PersonaOrProfile +import com.muwire.gui.profile.PersonaOrProfileCellRenderer +import com.muwire.gui.profile.PersonaOrProfileComparator import griffon.core.artifact.GriffonView import net.i2p.data.Destination @@ -95,7 +98,7 @@ class SearchTabView { scrollPane (constraints : BorderLayout.CENTER) { sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) { tableModel(list : model.senders) { - closureColumn(header : trans("SENDER"), preferredWidth : 500, type: Persona, read : { SenderBucket row -> row.sender}) + closureColumn(header : trans("SENDER"), preferredWidth : 500, type: PersonaOrProfile, read : { SenderBucket row -> row}) closureColumn(header : trans("RESULTS"), preferredWidth : 20, type: Integer, read : {SenderBucket row -> row.results.size()}) closureColumn(header : trans("BROWSE"), preferredWidth : 20, type: Boolean, read : {SenderBucket row -> row.results[0].browse}) closureColumn(header : trans("COLLECTIONS"), preferredWidth : 20, type: Boolean, read : {SenderBucket row -> row.results[0].browseCollections}) @@ -394,12 +397,12 @@ class SearchTabView { }) // senders table - def personaRenderer = new PersonaCellRenderer() - def personaComparator = new PersonaComparator() + def popRenderer = new PersonaOrProfileCellRenderer() + def popComparator = new PersonaOrProfileComparator() sendersTable.addMouseListener(sendersMouseListener) sendersTable.setDefaultRenderer(Integer.class, centerRenderer) - sendersTable.setDefaultRenderer(Persona.class, personaRenderer) - sendersTable.rowSorter.setComparator(0, personaComparator) + sendersTable.setDefaultRenderer(PersonaOrProfile.class, popRenderer) + sendersTable.rowSorter.setComparator(0, popComparator) sendersTable.rowSorter.addRowSorterListener({evt -> lastSendersSortEvent = evt}) sendersTable.rowSorter.setSortsOnUpdates(true) selectionModel = sendersTable.getSelectionModel() diff --git a/gui/src/main/groovy/com/muwire/gui/profile/PersonaOrProfileCellRenderer.groovy b/gui/src/main/groovy/com/muwire/gui/profile/PersonaOrProfileCellRenderer.groovy new file mode 100644 index 00000000..5e1031be --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/profile/PersonaOrProfileCellRenderer.groovy @@ -0,0 +1,46 @@ +package com.muwire.gui.profile + +import com.muwire.core.Persona + +import javax.swing.JTable +import javax.swing.table.DefaultTableCellRenderer +import java.awt.Component + +import static com.muwire.gui.Translator.trans + +class PersonaOrProfileCellRenderer extends DefaultTableCellRenderer { + @Override + Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) + + PersonaOrProfile pop = (PersonaOrProfile) value + if (pop.getThumbnail() != null) + setIcon(pop.getThumbnail()) + else + setIcon(null) + + if (pop.getTitle() != null) + setToolTipText(pop.getTitle()) + else + setToolTipText(trans("NO_PROFILE")) + + Persona persona = pop.getPersona() + setText("${htmlize(persona)}") + if (isSelected) { + setForeground(table.getSelectionForeground()) + setBackground(table.getSelectionBackground()) + } else { + setForeground(table.getForeground()) + setBackground(table.getBackground()) + } + this + } + + static String htmlize(Persona persona) { + int atIdx = persona.getHumanReadableName().indexOf("@") + String nickname = persona.getHumanReadableName().substring(0, atIdx) + String hashPart = persona.getHumanReadableName().substring(atIdx) + "$nickname$hashPart" + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/profile/PersonaOrProfileComparator.groovy b/gui/src/main/groovy/com/muwire/gui/profile/PersonaOrProfileComparator.groovy new file mode 100644 index 00000000..a21f54b0 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/profile/PersonaOrProfileComparator.groovy @@ -0,0 +1,9 @@ +package com.muwire.gui.profile + +import java.text.Collator + +class PersonaOrProfileComparator implements Comparator{ + int compare(PersonaOrProfile a, PersonaOrProfile b) { + Collator.getInstance().compare(a.getPersona().getHumanReadableName(), b.getPersona().getHumanReadableName()) + } +} diff --git a/gui/src/main/java/com/muwire/gui/profile/PersonaOrProfile.java b/gui/src/main/java/com/muwire/gui/profile/PersonaOrProfile.java new file mode 100644 index 00000000..32bea9f3 --- /dev/null +++ b/gui/src/main/java/com/muwire/gui/profile/PersonaOrProfile.java @@ -0,0 +1,11 @@ +package com.muwire.gui.profile; + +import com.muwire.core.Persona; + +import javax.swing.*; + +public interface PersonaOrProfile { + Persona getPersona(); + Icon getThumbnail(); + String getTitle(); +} diff --git a/gui/src/main/java/com/muwire/gui/profile/ThumbnailIcon.java b/gui/src/main/java/com/muwire/gui/profile/ThumbnailIcon.java new file mode 100644 index 00000000..9d677965 --- /dev/null +++ b/gui/src/main/java/com/muwire/gui/profile/ThumbnailIcon.java @@ -0,0 +1,33 @@ +package com.muwire.gui.profile; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class ThumbnailIcon implements Icon { + + BufferedImage image; + + public ThumbnailIcon(byte [] rawData) throws IOException { + BufferedImage img = ImageIO.read(new ByteArrayInputStream(rawData)); + image = ImageScaler.scaleToThumbnail(img); + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + ((Graphics2D)g).drawImage(image, x, y, null); + } + + @Override + public int getIconWidth() { + return image.getWidth(); + } + + @Override + public int getIconHeight() { + return image.getHeight(); + } +}