From 388cad0b5d3dbd512c1fea2ed390a9f9737e052a Mon Sep 17 00:00:00 2001 From: Zlatin Balevsky Date: Tue, 31 May 2022 20:01:16 +0100 Subject: [PATCH] wip on profile viewer and fetching full profiles --- .../main/groovy/com/muwire/core/Core.groovy | 14 +- .../core/connection/ConnectionAcceptor.groovy | 46 +++++- .../core/profile/MWProfileFetchEvent.groovy | 11 ++ .../core/profile/MWProfileFetcher.groovy | 83 +++++++++++ .../core/profile/UIProfileFetchEvent.groovy | 9 ++ .../core/profile/MWProfileFetchStatus.java | 5 + .../connection/ConnectionAcceptorTest.groovy | 2 +- gui/griffon-app/conf/Config.groovy | 5 + .../com/muwire/gui/SearchTabController.groovy | 23 ++- .../gui/profile/ViewProfileController.groovy | 45 ++++++ gui/griffon-app/i18n/messages.properties | 17 ++- .../com/muwire/gui/SearchTabModel.groovy | 2 +- .../gui/profile/ViewProfileModel.groovy | 55 ++++++++ .../views/com/muwire/gui/SearchTabView.groovy | 12 +- .../muwire/gui/profile/ViewProfileView.groovy | 132 ++++++++++++++++++ 15 files changed, 445 insertions(+), 16 deletions(-) create mode 100644 core/src/main/groovy/com/muwire/core/profile/MWProfileFetchEvent.groovy create mode 100644 core/src/main/groovy/com/muwire/core/profile/MWProfileFetcher.groovy create mode 100644 core/src/main/groovy/com/muwire/core/profile/UIProfileFetchEvent.groovy create mode 100644 core/src/main/java/com/muwire/core/profile/MWProfileFetchStatus.java create mode 100644 gui/griffon-app/controllers/com/muwire/gui/profile/ViewProfileController.groovy create mode 100644 gui/griffon-app/models/com/muwire/gui/profile/ViewProfileModel.groovy create mode 100644 gui/griffon-app/views/com/muwire/gui/profile/ViewProfileView.groovy diff --git a/core/src/main/groovy/com/muwire/core/Core.groovy b/core/src/main/groovy/com/muwire/core/Core.groovy index e2417200..cfde13b6 100644 --- a/core/src/main/groovy/com/muwire/core/Core.groovy +++ b/core/src/main/groovy/com/muwire/core/Core.groovy @@ -10,6 +10,9 @@ import com.muwire.core.messenger.UIFolderCreateEvent import com.muwire.core.messenger.UIFolderDeleteEvent import com.muwire.core.messenger.UIMessageMovedEvent import com.muwire.core.profile.MWProfile +import com.muwire.core.profile.MWProfileFetcher +import com.muwire.core.profile.MWProfileHeader +import com.muwire.core.profile.UIProfileFetchEvent import com.muwire.core.update.AutoUpdater import java.nio.charset.StandardCharsets @@ -290,6 +293,9 @@ public class Core { } else log.info("no profile exists for ${me.getHumanReadableName()}") + Supplier profileSupplier = this::getMyProfile + Supplier profileHeaderSupplier = {getMyProfile()?.getHeader()} as Supplier + eventBus = new EventBus() log.info("initializing i2p connector") @@ -430,7 +436,7 @@ public class Core { eventBus.register(UIFeedUpdateEvent.class, feedClient) log.info "initializing results sender" - ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, { getMyProfile()?.getHeader() } as Supplier, + ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, profileHeaderSupplier, props, certificateManager, chatServer, collectionManager) log.info "initializing search manager" @@ -480,7 +486,7 @@ public class Core { I2PAcceptor i2pAcceptor = new I2PAcceptor(i2pConnector::getSocketManager) eventBus.register(RouterConnectedEvent.class, i2pAcceptor) eventBus.register(RouterDisconnectedEvent.class, i2pAcceptor) - connectionAcceptor = new ConnectionAcceptor(eventBus, me, connectionManager, props, + connectionAcceptor = new ConnectionAcceptor(eventBus, me, profileSupplier, connectionManager, props, i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher, certificateManager, chatServer, collectionManager, isVisible) @@ -493,6 +499,10 @@ public class Core { BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me) eventBus.register(UIBrowseEvent.class, browseManager) + log.info("initializing profile fetcher") + MWProfileFetcher profileFetcher = new MWProfileFetcher(i2pConnector, eventBus, me, profileHeaderSupplier) + eventBus.register(UIProfileFetchEvent.class, profileFetcher) + log.info("initializing watched directory converter") watchedDirectoryConverter = new WatchedDirectoryConverter(this) diff --git a/core/src/main/groovy/com/muwire/core/connection/ConnectionAcceptor.groovy b/core/src/main/groovy/com/muwire/core/connection/ConnectionAcceptor.groovy index f3f01154..219fb44c 100644 --- a/core/src/main/groovy/com/muwire/core/connection/ConnectionAcceptor.groovy +++ b/core/src/main/groovy/com/muwire/core/connection/ConnectionAcceptor.groovy @@ -1,13 +1,15 @@ package com.muwire.core.connection +import com.muwire.core.profile.MWProfile import com.muwire.core.profile.MWProfileHeader import net.i2p.I2PException +import net.i2p.data.ByteArray import java.nio.charset.StandardCharsets -import java.nio.file.attribute.DosFileAttributes import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.function.BiPredicate +import java.util.function.Supplier import java.util.logging.Level import java.util.zip.DeflaterOutputStream import java.util.zip.GZIPInputStream @@ -54,6 +56,7 @@ class ConnectionAcceptor { final EventBus eventBus final Persona me + final Supplier myProfile final UltrapeerConnectionManager manager final MuWireSettings settings final I2PAcceptor acceptor @@ -75,13 +78,14 @@ class ConnectionAcceptor { volatile int browsed - ConnectionAcceptor(EventBus eventBus, Persona me, UltrapeerConnectionManager manager, + ConnectionAcceptor(EventBus eventBus, Persona me, Supplier myProfile, UltrapeerConnectionManager manager, MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache, TrustService trustService, SearchManager searchManager, UploadManager uploadManager, FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager, ChatServer chatServer, CollectionManager collectionManager, BiPredicate isVisible) { this.eventBus = eventBus this.me = me + this.myProfile = myProfile this.manager = manager this.settings = settings this.acceptor = acceptor @@ -188,6 +192,8 @@ class ConnectionAcceptor { case (byte)'L': processETTER(e) break + case (byte)'A': + procesVATAR(e) default: throw new Exception("Invalid read $read") } @@ -775,5 +781,41 @@ class ConnectionAcceptor { e.close() } } + + private void processVATAR(Endpoint e) { + byte [] VATAR = "VATAR\r\n".getBytes(StandardCharsets.US_ASCII) + byte [] read = new byte[VATAR.length] + + DataInputStream dis = new DataInputStream(e.getInputStream()) + try { + dis.readFully(read) + if (VATAR != read) + throw new Exception("Invalid VATAR") + + Map headers = DataUtil.readAllHeaders(dis) + if (headers['Version'] != "1") + throw new IOException("unrecognized version") + + OutputStream os = e.getOutputStream() + MWProfile profile = myProfile.get() + if (profile == null) { + os.write("404 Not Found\r\n".getBytes(StandardCharsets.US_ASCII)) + return + } + + os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII)) + + ByteArrayOutputStream baos = new ByteArrayInputStream() + profile.write(baos) + byte [] payload = baos.toByteArray() + os.write("Length:${payload.length}\r\n".getBytes(StandardCharsets.US_ASCII)) + os.write("\r\n".getBytes(StandardCharsets.US_ASCII)) + os.write(payload) + } catch (Exception bad) { + log.log(Level.WARNING, "failed to process AVATAR", bad) + } finally { + e.close() + } + } } diff --git a/core/src/main/groovy/com/muwire/core/profile/MWProfileFetchEvent.groovy b/core/src/main/groovy/com/muwire/core/profile/MWProfileFetchEvent.groovy new file mode 100644 index 00000000..3fda9816 --- /dev/null +++ b/core/src/main/groovy/com/muwire/core/profile/MWProfileFetchEvent.groovy @@ -0,0 +1,11 @@ +package com.muwire.core.profile + +import com.muwire.core.Event +import com.muwire.core.Persona + +class MWProfileFetchEvent extends Event { + MWProfileFetchStatus status + Persona host + UUID uuid + MWProfile profile +} diff --git a/core/src/main/groovy/com/muwire/core/profile/MWProfileFetcher.groovy b/core/src/main/groovy/com/muwire/core/profile/MWProfileFetcher.groovy new file mode 100644 index 00000000..b37728da --- /dev/null +++ b/core/src/main/groovy/com/muwire/core/profile/MWProfileFetcher.groovy @@ -0,0 +1,83 @@ +package com.muwire.core.profile + +import com.muwire.core.Constants +import com.muwire.core.EventBus +import com.muwire.core.Persona +import com.muwire.core.connection.Endpoint +import com.muwire.core.connection.I2PConnector +import com.muwire.core.util.DataUtil +import groovy.util.logging.Log + +import java.nio.charset.StandardCharsets +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.function.Supplier +import java.util.logging.Level + +@Log +class MWProfileFetcher { + + private final I2PConnector connector + private final EventBus eventBus + private final Persona me + private final Supplier myProfileHeader + + private final Executor fetcherThread = Executors.newCachedThreadPool() + + MWProfileFetcher(I2PConnector connector, EventBus eventBus, + Persona me, Supplier myProfileHeader) { + this.connector = connector + this.eventBus = eventBus + this.me = me + this.myProfileHeader = myProfileHeader + } + + void onUIProfileFetchEvent(UIProfileFetchEvent e) { + fetcherThread.execute({ + Endpoint endpoint = null + try { + eventBus.publish(new MWProfileFetchEvent(host: e.host, status: MWProfileFetchStatus.CONNECTING, uuid: e.uuid)) + endpoint = connector.connect(e.host.destination) + + + OutputStream os = endpoint.getOutputStream() + os.write("AVATAR\r\n".getBytes(StandardCharsets.US_ASCII)) + os.write("Version:1\r\n".getBytes(StandardCharsets.US_ASCII)) + os.write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII)) + MWProfileHeader header = myProfileHeader.get() + if (header != null) + os.write("ProfileHeader:${header.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII)) + os.write("\r\n".getBytes(StandardCharsets.US_ASCII)) + + InputStream is = endpoint.getInputStream() + String code = DataUtil.readTillRN(is) + if (!code.startsWith("200")) + throw new IOException("invalid code $code") + + Map headers = DataUtil.readAllHeaders(is) + + if (!headers.containsKey("Length")) + throw new IOException("No length header") + + int length = Integer.parseInt(headers['Length']) + if (length > Constants.MAX_PROFILE_LENGTH) + throw new IOException("profile too large $length") + + eventBus.publish(new MWProfileFetchEvent(host: e.host, status: MWProfileFetchStatus.FETCHING, uuid: e.uuid)) + byte[] payload = new byte[length] + DataInputStream dis = new DataInputStream(is) + dis.readFully(payload) + MWProfile profile = new MWProfile(new ByteArrayInputStream(payload)) + if (profile.getHeader().getPersona() != e.host) + throw new Exception("profile and host mismatch") + eventBus.publish(new MWProfileFetchEvent(host: e.host, status: MWProfileFetchStatus.FINISHED, + uuid: e.uuid, profile: profile)) + } catch (Exception bad) { + log.log(Level.WARNING, "profile fetch failed", bad) + eventBus.publish(new MWProfileFetchEvent(host: e.host, status: MWProfileFetchStatus.FAILED, uuid: e.uuid)) + } finally { + endpoint?.close() + } + } as Runnable) + } +} diff --git a/core/src/main/groovy/com/muwire/core/profile/UIProfileFetchEvent.groovy b/core/src/main/groovy/com/muwire/core/profile/UIProfileFetchEvent.groovy new file mode 100644 index 00000000..3c077927 --- /dev/null +++ b/core/src/main/groovy/com/muwire/core/profile/UIProfileFetchEvent.groovy @@ -0,0 +1,9 @@ +package com.muwire.core.profile + +import com.muwire.core.Event +import com.muwire.core.Persona + +class UIProfileFetchEvent extends Event { + UUID uuid + Persona host +} diff --git a/core/src/main/java/com/muwire/core/profile/MWProfileFetchStatus.java b/core/src/main/java/com/muwire/core/profile/MWProfileFetchStatus.java new file mode 100644 index 00000000..83abcbb3 --- /dev/null +++ b/core/src/main/java/com/muwire/core/profile/MWProfileFetchStatus.java @@ -0,0 +1,5 @@ +package com.muwire.core.profile; + +public enum MWProfileFetchStatus { + CONNECTING, FETCHING, FAILED, FINISHED +} diff --git a/core/src/test/groovy/com/muwire/core/connection/ConnectionAcceptorTest.groovy b/core/src/test/groovy/com/muwire/core/connection/ConnectionAcceptorTest.groovy index 0d08459d..fc4f08ab 100644 --- a/core/src/test/groovy/com/muwire/core/connection/ConnectionAcceptorTest.groovy +++ b/core/src/test/groovy/com/muwire/core/connection/ConnectionAcceptorTest.groovy @@ -99,7 +99,7 @@ class ConnectionAcceptorTest { uploadManager = uploadManagerMock.proxyInstance() connectionEstablisher = connectionEstablisherMock.proxyInstance() - acceptor = new ConnectionAcceptor(eventBus, null, connectionManager, settings, i2pAcceptor, + acceptor = new ConnectionAcceptor(eventBus, null, null, connectionManager, settings, i2pAcceptor, hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null, null, null, {f, p -> true} as BiPredicate) acceptor.start() diff --git a/gui/griffon-app/conf/Config.groovy b/gui/griffon-app/conf/Config.groovy index 13e0b818..30c84f14 100644 --- a/gui/griffon-app/conf/Config.groovy +++ b/gui/griffon-app/conf/Config.groovy @@ -243,4 +243,9 @@ mvcGroups { view = 'com.muwire.gui.profile.EditProfileView' controller = 'com.muwire.gui.profile.EditProfileController' } + 'view-profile' { + model = 'com.muwire.gui.profile.ViewProfileModel' + view = 'com.muwire.gui.profile.ViewProfileView' + controller = 'com.muwire.gui.profile.ViewProfileController' + } } diff --git a/gui/griffon-app/controllers/com/muwire/gui/SearchTabController.groovy b/gui/griffon-app/controllers/com/muwire/gui/SearchTabController.groovy index 7aec33d1..bff6f7b5 100644 --- a/gui/griffon-app/controllers/com/muwire/gui/SearchTabController.groovy +++ b/gui/griffon-app/controllers/com/muwire/gui/SearchTabController.groovy @@ -90,6 +90,7 @@ class SearchTabController { def sender = view.selectedSender() if (sender == null) return + sender = sender.getPersona() String reason = JOptionPane.showInputDialog("Enter reason (optional)") core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED, reason : reason)) } @@ -99,16 +100,25 @@ class SearchTabController { def sender = view.selectedSender() if (sender == null) return + sender = sender.getPersona() String reason = JOptionPane.showInputDialog("Enter reason (optional)") core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED, reason : reason)) } @ControllerAction - void neutral() { + void viewProfile() { def sender = view.selectedSender() if (sender == null) return - core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL)) + + UUID uuid = UUID.randomUUID() + def params = [:] + params.core = model.core + params.persona = sender.getPersona() + params.uuid = uuid + params.profileTitle = sender.getTitle() + + mvcGroup.createMVCGroup("view-profile", uuid.toString(), params) } @ControllerAction @@ -117,6 +127,7 @@ class SearchTabController { if (sender == null) return + sender = sender.getPersona() String groupId = UUID.randomUUID().toString() Map params = new HashMap<>() params['host'] = sender @@ -131,6 +142,7 @@ class SearchTabController { if (sender == null) return + sender = sender.getPersona() UUID uuid = UUID.randomUUID() def params = [:] params['fileName'] = sender.getHumanReadableName() @@ -147,6 +159,7 @@ class SearchTabController { if (sender == null) return + sender = sender.getPersona() Feed feed = new Feed(sender) feed.setAutoDownload(core.muOptions.defaultFeedAutoDownload) feed.setSequential(core.muOptions.defaultFeedSequential) @@ -163,6 +176,7 @@ class SearchTabController { if (sender == null) return + sender = sender.getPersona() def parent = mvcGroup.parentGroup parent.controller.startChat(sender) parent.view.showChatWindow.call() @@ -170,7 +184,8 @@ class SearchTabController { @ControllerAction void message() { - Persona recipient = view.selectedSender() + Persona recipient = view.selectedSender()?.getPersona() + if (recipient == null) return @@ -182,7 +197,7 @@ class SearchTabController { @ControllerAction void copyFullID() { - Persona sender = view.selectedSender() + Persona sender = view.selectedSender()?.getPersona() if (sender == null) return CopyPasteSupport.copyToClipboard(sender.toBase64()) diff --git a/gui/griffon-app/controllers/com/muwire/gui/profile/ViewProfileController.groovy b/gui/griffon-app/controllers/com/muwire/gui/profile/ViewProfileController.groovy new file mode 100644 index 00000000..2e79c60a --- /dev/null +++ b/gui/griffon-app/controllers/com/muwire/gui/profile/ViewProfileController.groovy @@ -0,0 +1,45 @@ +package com.muwire.gui.profile + +import com.muwire.core.trust.TrustEvent +import com.muwire.core.trust.TrustLevel +import griffon.core.artifact.GriffonController +import griffon.core.controller.ControllerAction +import griffon.inject.MVCMember +import griffon.metadata.ArtifactProviderFor + +import static com.muwire.gui.Translator.trans + +import javax.annotation.Nonnull +import javax.swing.JOptionPane + +@ArtifactProviderFor(GriffonController) +class ViewProfileController { + + @MVCMember @Nonnull + ViewProfileModel model + @MVCMember @Nonnull + ViewProfileView view + + @ControllerAction + void fetch() { + model.register() + } + + @ControllerAction + void addContact() { + String reason = JOptionPane.showInputDialog(trans("ENTER_REASON_OPTIONAL")) + model.core.eventBus.publish(new TrustEvent(persona: model.persona, level: TrustLevel.TRUSTED, reason: reason)) + } + + @ControllerAction + void block() { + String reason = JOptionPane.showInputDialog(trans("ENTER_REASON_OPTIONAL")) + model.core.eventBus.publish(new TrustEvent(persona: model.persona, level: TrustLevel.DISTRUSTED, reason: reason)) + } + + @ControllerAction + void close() { + view.window.setVisible(false) + mvcGroup.destroy() + } +} diff --git a/gui/griffon-app/i18n/messages.properties b/gui/griffon-app/i18n/messages.properties index 6c53fabd..5ceb4a31 100644 --- a/gui/griffon-app/i18n/messages.properties +++ b/gui/griffon-app/i18n/messages.properties @@ -271,7 +271,7 @@ NEUTRAL=Neutral DISTRUST=Distrust COPY_FULL_ID=Copy Full ID NO_PROFILE=This user does not have a profile - +VIEW_PROFILE=View Profile # results table (group by sender) DIRECT_SOURCES=Direct Sources @@ -712,6 +712,15 @@ PROFILE_EDITOR_ERROR_NO_IMAGE=Please select an avatar or generate one PROFILE_EDITOR_ERROR_LARGE_TITLE=Profile title is too long PROFILE_EDITOR_ERROR_LARGE_BODY=Profile is too long +## View Profile Frame +PROFILE_VIEWER_TITLE=Profile of {0} +PROFILE_VIEWER_HEADER=Profile Title +PROFILE_VIEWER_HEADER_MISSING=No profile title available. Fetch profile to see the title. +PROFILE_VIEWER_FETCH=Fetch Profile +PROFILE_VIEWER_AVATAR=User Avatar +PROFILE_VIEWER_PROFILE=Profile +PROFILE_VIEWER_BLOCK=Block + ## Tooltips TOOLTIP_FILE_FEED_DISABLED=Your file feed is disabled @@ -727,6 +736,7 @@ TOOLTIP_BROWSE_COLLECTIONS_SENDER=Browse all the sender's collections TOOLTIP_SENDER_COPY_FULL=Copy the full ID of the sender TOOLTIP_ADD_CONTACT_SENDER=Add the sender as a trusted contact TOOLTIP_DISTRUST_SENDER=Block the sender +TOOLTIP_VIEW_PROFILE=View the profile of the sender TOOLTIP_DOWNLOAD_FILE=Download the selected results TOOLTIP_DOWNLOAD_SEQUENTIALLY=Download in order (enables preview) TOOLTIP_VIEW_DETAILS_RESULT=View details about the result @@ -886,5 +896,10 @@ TOOLTIP_CHAT_SERVERS_CONNECT=Connect to server now TOOLTIP_PROFILE_EDITOR=Edit Profile TOOLTIP_PROFILE_EDITOR_GENERATE=Generate an avatar from your MuWire ID +### Tooltips for the profile viewer +TOOLTIP_PROFILE_VIEWER_FETCH=Fetch the full profile of this user +TOOLTIP_PROFILE_VIEWER_ADD_CONTACT=Add this user as a trusted contact +TOOLTIP_PROFILE_VIEWER_BLOCK=Block this user + ## Test string TEST_PLURALIZABLE_STRING={count, plural, one {You have {count} item.} other {You have {count} items.}} diff --git a/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy b/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy index 329ee747..b1a7560e 100644 --- a/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy @@ -39,6 +39,7 @@ class SearchTabModel { @Observable boolean chatActionEnabled @Observable boolean subscribeActionEnabled @Observable boolean messageActionEnabled + @Observable boolean viewProfileActionEnabled @Observable boolean viewDetailsActionEnabled @Observable boolean groupedByFile @@ -62,7 +63,6 @@ class SearchTabModel { def results2 = [] def allResults2 = new LinkedHashSet() - def senders2 = [] volatile String[] filter volatile Filterer filterer @Observable boolean clearFilterActionEnabled diff --git a/gui/griffon-app/models/com/muwire/gui/profile/ViewProfileModel.groovy b/gui/griffon-app/models/com/muwire/gui/profile/ViewProfileModel.groovy new file mode 100644 index 00000000..6117a188 --- /dev/null +++ b/gui/griffon-app/models/com/muwire/gui/profile/ViewProfileModel.groovy @@ -0,0 +1,55 @@ +package com.muwire.gui.profile + +import com.muwire.core.Core +import com.muwire.core.Persona +import com.muwire.core.profile.MWProfileFetchEvent +import com.muwire.core.profile.MWProfileFetchStatus +import com.muwire.core.profile.MWProfileHeader +import com.muwire.core.profile.UIProfileFetchEvent +import griffon.core.artifact.GriffonModel +import griffon.inject.MVCMember +import griffon.metadata.ArtifactProviderFor +import griffon.transform.Observable + +import javax.annotation.Nonnull + +@ArtifactProviderFor(GriffonModel) +class ViewProfileModel { + @MVCMember @Nonnull + ViewProfileView view + + Core core + Persona persona + UUID uuid + String profileTitle + + @Observable MWProfileFetchStatus status + + private boolean registered + + void mvcGroupInit(Map args) { + } + + void register() { + if (registered) + return + registered = true + core.getEventBus().register(MWProfileFetchEvent.class, this) + core.getEventBus().publish(new UIProfileFetchEvent(uuid: uuid, host: persona)) + } + + void mvcGroupDestroy() { + if (registered) + core.getEventBus().unregister(MWProfileFetchEvent.class, this) + } + + void onMWProfileFetchEvent(MWProfileFetchEvent event) { + if (uuid != event.uuid) + return + runInsideUIAsync { + status = event.status + if (status == MWProfileFetchStatus.FINISHED) + view.profileFetched(event.profile) + } + } +} diff --git a/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy b/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy index 3e4e7777..3b067a5f 100644 --- a/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy @@ -126,6 +126,7 @@ class SearchTabView { panel (border : etchedBorder()){ button(text : trans("ADD_CONTACT"), toolTipText: trans("TOOLTIP_ADD_CONTACT_SENDER"), enabled: bind {model.trustButtonsEnabled }, trustAction) button(text : trans("DISTRUST"), toolTipText: trans("TOOLTIP_DISTRUST_SENDER"), enabled : bind {model.trustButtonsEnabled}, distrustAction) + button(text : trans("VIEW_PROFILE"), toolTipText: trans("TOOLTIP_VIEW_PROFILE"), enabled: bind {model.viewProfileActionEnabled}, viewProfileAction) } } } @@ -412,6 +413,7 @@ class SearchTabView { int row = selectedSenderRow() if (row < 0) { model.trustButtonsEnabled = false + model.viewProfileActionEnabled = false model.browseActionEnabled = false model.subscribeActionEnabled = false model.browseCollectionsActionEnabled = false @@ -428,6 +430,7 @@ class SearchTabView { model.subscribeActionEnabled = bucket.results[0].feed && model.core.feedManager.getFeed(sender) == null model.trustButtonsEnabled = true + model.viewProfileActionEnabled = true model.results.clear() model.results.addAll(bucket.results) @@ -469,6 +472,7 @@ class SearchTabView { model.browseCollectionsActionEnabled = false model.chatActionEnabled = false model.messageActionEnabled = false + model.viewProfileActionEnabled = false return } @@ -525,6 +529,7 @@ class SearchTabView { model.tab = parent.indexOfComponent(pane) parent.removeTabAt(model.tab) model.trustButtonsEnabled = false + model.viewProfileActionEnabled = false model.downloadActionEnabled = false resultDetails.values().each {it.destroy()} mvcGroup.destroy() @@ -680,14 +685,11 @@ class SearchTabView { } } - Persona selectedSender() { + PersonaOrProfile selectedSender() { int row = selectedSenderRow() if (row < 0) return null - if (model.groupedByFile) - return model.senders2[row]?.sender - else - return model.senders[row]?.sender + return model.senders[row] } def showSenderGrouping = { diff --git a/gui/griffon-app/views/com/muwire/gui/profile/ViewProfileView.groovy b/gui/griffon-app/views/com/muwire/gui/profile/ViewProfileView.groovy new file mode 100644 index 00000000..00daa07d --- /dev/null +++ b/gui/griffon-app/views/com/muwire/gui/profile/ViewProfileView.groovy @@ -0,0 +1,132 @@ +package com.muwire.gui.profile + +import com.muwire.core.profile.MWProfile +import com.muwire.gui.HTMLSanitizer + +import javax.imageio.ImageIO +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTextArea +import javax.swing.border.TitledBorder +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Window +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent + +import static com.muwire.gui.Translator.trans + +import griffon.core.GriffonApplication +import griffon.core.artifact.GriffonView +import griffon.inject.MVCMember +import griffon.metadata.ArtifactProviderFor + +import javax.annotation.Nonnull +import javax.inject.Inject +import javax.swing.JFrame + +@ArtifactProviderFor(GriffonView) +class ViewProfileView { + @MVCMember @Nonnull + ViewProfileModel model + @MVCMember @Nonnull + ViewProfileController controller + @MVCMember @Nonnull + FactoryBuilderSupport builder + @Inject + GriffonApplication application + + JFrame window + JFrame mainFrame + + JPanel mainPanel + JLabel titleLabel + JPanel imagePanel + JTextArea bodyArea + + void initUI() { + mainFrame = application.windowManager.findWindow("main-frame") + def mainDim = mainFrame.getSize() + + int dimX = Math.max(1100, (int)(mainDim.getWidth() / 2)) + int dimY = Math.max(700, (int)(mainDim.getHeight() / 2)) + + window = builder.frame(visible: false, defaultCloseOperation: JFrame.DISPOSE_ON_CLOSE, + iconImage: builder.imageIcon("/MuWire-48x48.png").image, + title: trans("PROFILE_VIEWER_TITLE", model.persona.getHumanReadableName())) { + borderLayout() + panel(border: titledBorder(title : trans("PROFILE_VIEWER_HEADER"), border: etchedBorder(), + titlePosition: TitledBorder.TOP), constraints: BorderLayout.NORTH) { + if (model.profileTitle == null) + titleLabel = label(text: trans("PROFILE_VIEWER_HEADER_MISSING")) + else + titleLabel = label(text: model.profileTitle) + } + mainPanel = panel(constraints: BorderLayout.CENTER) { + cardLayout() + panel(constraints: "fetch-profile") { + button(text: trans("PROFILE_VIEWER_FETCH"), toolTipText: trans("TOOLTIP_PROFILE_VIEWER_FETCH"), + fetchAction) + } + panel(constraints: "full-profile") { + gridLayout(rows: 1, cols: 2) + panel(border: titledBorder(title: trans("PROFILE_VIEWER_AVATAR"), border: etchedBorder(), + titlePosition: TitledBorder.TOP)) { + imagePanel = panel() + } + panel(border: titledBorder(title: trans("PROFILE_VIEWER_PROFILE"), border: etchedBorder(), + titlePosition: TitledBorder.TOP)) { + scrollPane { + bodyArea = textArea(editable: false, lineWrap: true, wrapStyleWord: true) + } + } + } + } + panel(constraints: BorderLayout.SOUTH) { + borderLayout() + panel(constraints: BorderLayout.WEST) { + label(text : bind { model.status == null ? "" : trans(model.status.name())}) + } + panel(constraints: BorderLayout.CENTER) { + button(text: trans("ADD_CONTACT"), toolTipText: trans("TOOLTIP_PROFILE_VIEWER_ADD_CONTACT"), + addContactAction) + button(text: trans("PROFILE_VIEWER_BLOCK"), toolTipText: trans("TOOLTIP_PROFILE_VIEWER_BLOCK"), + blockAction) + } + panel(constraints: BorderLayout.EAST) { + button(text : trans("CLOSE"), closeAction) + } + } + + } + window.setPreferredSize([dimX, dimY] as Dimension) + } + + void mvcGroupInit(Map params) { + window.addWindowListener(new WindowAdapter() { + @Override + void windowClosed(WindowEvent e) { + mvcGroup.destroy() + } + }) + + window.pack() + window.setLocationRelativeTo(mainFrame) + window.setVisible(true) + } + + void profileFetched(MWProfile profile) { + mainPanel.getLayout().show(mainPanel, "full-profile") + titleLabel.setText(HTMLSanitizer.sanitize(profile.getHeader().getTitle())) + bodyArea.setText(profile.getBody()) + + def rawImage = ImageIO.read(new ByteArrayInputStream(profile.getImage())) + def mainImage = ImageScaler.scaleToMax(rawImage) + + def imgDim = imagePanel.getSize() + imagePanel.getGraphics().drawImage(mainImage, + (int)(imgDim.getWidth() / 2) - (int)(mainImage.getWidth() / 2), + (int)(imgDim.getHeight() / 2) - (int)(mainImage.getHeight() / 2), + null) + } +}