From 94b2688cd758dc4ae6e35d0862c4f1cf83e3f83d Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Thu, 13 Oct 2016 16:33:02 +0200 Subject: [PATCH 01/15] generation and searching for uri via regex and replace them with the delimiters of HyperLinkLabel that he can build clickable links out of it. and add the eventlistener to open the link in browser --- .../renderer/PlaintextMessageRenderer.java | 57 ++++++++++++++++++- .../PlaintextMessageRendererTest.java | 28 +++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index 01bcff3b..a3a13a71 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -2,15 +2,66 @@ import de.qabel.desktop.daemon.drop.TextMessage; import javafx.scene.Node; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; +import javafx.scene.text.TextFlow; +import org.controlsfx.control.HyperlinkLabel; +import org.jetbrains.annotations.NotNull; +import java.awt.*; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ResourceBundle; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class PlaintextMessageRenderer implements FXMessageRenderer { + private static final String STYLE_CLASS = "message-text"; + public Consumer browserOpener; + + public PlaintextMessageRenderer() { + browserOpener = (uri) -> { + try { + Desktop.getDesktop().browse(new URI(uri)); + } catch (IOException | URISyntaxException ignored) { + } + }; + + } + @Override public Node render(String dropPayload, ResourceBundle resourceBundle) { - Label label = new Label(renderString(dropPayload, resourceBundle)); - label.getStyleClass().add("message-text"); + String text = renderString(dropPayload, resourceBundle); + return renderHyperlinks(text); + } + + @NotNull + TextFlow renderHyperlinks(String message) { + String regex = "(https?:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"; + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.COMMENTS | Pattern.MULTILINE); + Matcher matcher = pattern.matcher(message); + String result = matcher.replaceAll("[$1]"); + + HyperlinkLabel hyperLinkLabel = new HyperlinkLabel(result); + hyperLinkLabel.getStyleClass().add("text"); + + hyperLinkLabel.setOnAction(event -> { + Hyperlink link = (Hyperlink) event.getSource(); + final String uri = link == null ? "" : link.getText(); + new Thread(() -> browserOpener.accept(uri)).start(); + }); + + TextFlow node = new TextFlow(hyperLinkLabel); + node.getStyleClass().add(STYLE_CLASS); + return node; + } + + @NotNull + private Label renderLabel(String text) { + Label label = new Label(text); + label.getStyleClass().add(STYLE_CLASS); return label; } @@ -18,4 +69,6 @@ public Node render(String dropPayload, ResourceBundle resourceBundle) { public String renderString(String dropPayload, ResourceBundle resourceBundle) { return TextMessage.fromJson(dropPayload).getText(); } + + } diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java index ac880542..da7bf833 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java @@ -3,33 +3,53 @@ import de.qabel.desktop.ui.AbstractFxTest; import javafx.scene.Node; import javafx.scene.control.Labeled; +import javafx.scene.text.TextFlow; +import org.controlsfx.control.HyperlinkLabel; import org.junit.Before; import org.junit.Test; +import java.util.concurrent.atomic.AtomicReference; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class PlaintextMessageRendererTest extends AbstractFxTest { private PlaintextMessageRenderer renderer; + private String payload; @Before public void setUp() throws Exception { renderer = new PlaintextMessageRenderer(); + payload = "{'msg': 'content'}"; } @Test public void rendersPlaintextFromJson() { - String payload = "{'msg': 'content'}"; String message = renderer.renderString(payload, null); - assertEquals("content", message); } @Test public void rendersMessageNode() { - String payload = "{'msg': 'content'}"; Node node = renderer.render(payload, null); assertTrue(node instanceof Labeled); - assertEquals("content", ((Labeled)node).getText()); + assertEquals("content", ((Labeled) node).getText()); + } + + @Test + public void renderHyperlinks() throws Exception { + final String string = "This is a Text,\n" + + " wich has a neat hyperlinks www.qabel.de"; + + String expectedUriFormat = "[www.qabel.de]"; + + AtomicReference browserOpener = new AtomicReference<>(); + renderer.browserOpener = browserOpener::set; + + Node node = renderer.renderHyperlinks(string); + HyperlinkLabel hyperLinkLabel = ((HyperlinkLabel) ((TextFlow) node).getChildren().get(0)); + assertTrue(hyperLinkLabel.getText().contains(expectedUriFormat)); } + + } From 17bf281328b702422ca126c930275572fcc7e454 Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Thu, 13 Oct 2016 16:33:18 +0200 Subject: [PATCH 02/15] css stuff to make it pretty --- src/main/resources/main.css | 92 ++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/src/main/resources/main.css b/src/main/resources/main.css index ef871538..656e1e20 100644 --- a/src/main/resources/main.css +++ b/src/main/resources/main.css @@ -155,14 +155,14 @@ -fx-background-radius: 5; } -#shadow { - -fx-effect: dropshadow(one-pass-box, grey, 5, -1, 0, 2); -} - #myMessagebox { -fx-background-color: #4d4d4d; -fx-background-radius: 5; } +#shadow { + -fx-effect: dropshadow(one-pass-box, grey, 5, -1, 0, 2); +} + .selected { -fx-background-color: #b3b3b3; @@ -254,35 +254,77 @@ -fx-text-fill: -fx-text-inner-color; } -.message-text { - -fx-padding: 1em 1em 1em 1em; - -fx-text-alignment: justify; - -fx-wrap-text: true; - -fx-text-fill: white; + +.hyperlink { + -fx-border-color: transparent; + -fx-padding: 0; + -fx-underline: false; } +.hyperlink .text { + -fx-fill: #222222; +} + +.hyperlink:visited .text { + -fx-fill: #222222; +} + +.hyperlink:hover .text { + -fx-fill: #787878; +} + +.hyperlink:pressed .text { + -fx-fill: #fd670d; +} + + .message-date { -fx-padding: 0 1em 0.5em 1em; -fx-text-alignment: justify; -fx-wrap-text: true; } + .own .message-date { -fx-alignment: bottom-right; -fx-text-alignment: right; } -.own .message-text .label { +.message-text { + -fx-padding: 1em 1em 1em 1em; + -fx-text-alignment: justify; + -fx-wrap-text: true; + -fx-text-fill: white; + -fx-fill: white; +} + +.message-text, +.message-text .label, +.message-text .text, +.message-text Text { + -fx-wrap-text: true; + -fx-text-fill: white; + -fx-fill: white; +} + +.own .message-text .label, +.own .message-text Text { -fx-text-alignment: right; + -fx-text-fill: #4d4d4d; } -.other .message-text .label { +.other .message-text .label , +.other .message-text Text { -fx-text-alignment: left; + } -.message-text .label { - -fx-wrap-text: true; - -fx-text-fill: white; +.own .message-text .hyperlink .text { + -fx-fill: #ff690f; +} + +.other .message-text .hyperlink .text { + -fx-fill: #4d4d4d; } .payload-type-icon { @@ -458,28 +500,6 @@ VBox.spaced, HBox.spaced { -fx-border-width: 0 0 1px 0; } -.hyperlink { - -fx-border-color: transparent; - -fx-padding: 0; - -fx-underline: false; -} - -.hyperlink .text { - -fx-fill: #222222; -} - -.hyperlink:visited .text { - -fx-fill: #222222; -} - -.hyperlink:hover .text { - -fx-fill: #787878; -} - -.hyperlink:pressed .text { - -fx-fill: #fd670d; -} - .labeled-link { -fx-spacing: 0.5em; } From ea8fc26af99b937f90523648df5a249240b37565 Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Thu, 13 Oct 2016 16:34:18 +0200 Subject: [PATCH 03/15] testing if an action event can be fired and the result contains an valid uri --- .../ui/actionlog/ActionlogGuiTest.java | 42 ++++++++++++++++--- .../desktop/ui/AbstractControllerTest.java | 10 +++-- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java index c0420d46..44da0086 100644 --- a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java +++ b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java @@ -13,25 +13,57 @@ import java.util.Date; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import static junit.framework.TestCase.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; public class ActionlogGuiTest extends AbstractGuiTest { + private AtomicReference browserOpener = new AtomicReference<>(); @Override protected FXMLView getView() { return new ActionlogView(); } + @Test + public void typeMessageWithHyperlinkAndClickIt() throws Exception { + controller.contact = new Contact(identity.getAlias(), identity.getDropUrls(), identity.getEcPublicKey()); + writeTwoLinesWithOneHyperlink(); + submitChat(); + + plaintextMessageRenderer.browserOpener = browserOpener::set; + fxMessageRendererFactory.setFallbackRenderer(plaintextMessageRenderer); + clickOn(".hyperlink"); + assertThat(browserOpener.get(), is("http://qabel.de")); + } + + private void submitChat() { + assertTrue(receiveMessages().isEmpty()); + robot.push(KeyCode.ENTER); + assertFalse(receiveMessages().isEmpty()); + } + + private void writeTwoLinesWithOneHyperlink() { + FxRobot textArea = clickOn("#textarea"); + textArea.write("line1"); + robot.press(KeyCode.SHIFT); + try { + robot.push(KeyCode.ENTER); + } finally { + robot.release(KeyCode.SHIFT); + } + robot.write("http://qabel.de"); + } + @Test public void testSendMessage() { String text = "Message"; waitUntil(() -> controller.textarea != null); Identity i = controller.identity; - controller.contact = new Contact(i.getAlias(),i.getDropUrls(), i.getEcPublicKey()); + controller.contact = new Contact(i.getAlias(), i.getDropUrls(), i.getEcPublicKey()); clickOn("#textarea").write(text); robot.push(KeyCode.ENTER); List list = receiveMessages(); @@ -58,9 +90,7 @@ public void multilineInput() { } robot.write("line2"); - assertTrue(receiveMessages().isEmpty()); - robot.push(KeyCode.ENTER); - assertFalse(receiveMessages().isEmpty()); + submitChat(); DropMessage message = receiveMessages().get(0); assertEquals(DropMessageRepository.PAYLOAD_TYPE_MESSAGE, message.getDropPayloadType()); diff --git a/src/test/java/de/qabel/desktop/ui/AbstractControllerTest.java b/src/test/java/de/qabel/desktop/ui/AbstractControllerTest.java index afb6e6fa..42b882da 100644 --- a/src/test/java/de/qabel/desktop/ui/AbstractControllerTest.java +++ b/src/test/java/de/qabel/desktop/ui/AbstractControllerTest.java @@ -88,6 +88,9 @@ public class AbstractControllerTest extends AbstractFxTest { protected int remoteDebounceTimeout; protected EventDispatcher eventDispatcher = new SubjectEventDispatcher(); + protected FXMessageRendererFactory fxMessageRendererFactory = new FXMessageRendererFactory(); + protected PlaintextMessageRenderer plaintextMessageRenderer = new PlaintextMessageRenderer(); + static { logger = createLogger(); } @@ -150,9 +153,10 @@ public void setUp() throws Exception { diContainer.put("boxSyncConfigRepository", boxSyncRepository); diContainer.put("boxSyncRepository", boxSyncRepository); diContainer.put("transactionManager", transactionManager); - FXMessageRendererFactory FXMessageRendererFactory = new FXMessageRendererFactory(); - FXMessageRendererFactory.setFallbackRenderer(new PlaintextMessageRenderer()); - diContainer.put("messageRendererFactory", FXMessageRendererFactory); + + fxMessageRendererFactory.setFallbackRenderer(plaintextMessageRenderer); + diContainer.put("messageRendererFactory", fxMessageRendererFactory); + SyncIndexFactory syncIndexFactory = new SqliteSyncIndexFactory(); diContainer.put("boxSyncConfigFactory", new DefaultBoxSyncConfigFactory(syncIndexFactory)); diContainer.put("boxSyncIndexFactory", syncIndexFactory); From 7c02c35171dd7fc040c1300b520f4d0ae3357c08 Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Mon, 17 Oct 2016 11:13:36 +0200 Subject: [PATCH 04/15] some refactoring --- .../renderer/PlaintextMessageRenderer.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index a3a13a71..f704d18c 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -1,6 +1,7 @@ package de.qabel.desktop.ui.actionlog.item.renderer; import de.qabel.desktop.daemon.drop.TextMessage; +import javafx.event.ActionEvent; import javafx.scene.Node; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; @@ -19,17 +20,13 @@ public class PlaintextMessageRenderer implements FXMessageRenderer { private static final String STYLE_CLASS = "message-text"; - public Consumer browserOpener; - - public PlaintextMessageRenderer() { - browserOpener = (uri) -> { - try { - Desktop.getDesktop().browse(new URI(uri)); - } catch (IOException | URISyntaxException ignored) { - } - }; - - } + public Consumer browserOpener = (uri) -> { + try { + Desktop.getDesktop().browse(new URI(uri)); + } catch (IOException | URISyntaxException ignored) { + } + }; + private static final String DETECT_URI = "(https?:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"; @Override public Node render(String dropPayload, ResourceBundle resourceBundle) { @@ -39,25 +36,28 @@ public Node render(String dropPayload, ResourceBundle resourceBundle) { @NotNull TextFlow renderHyperlinks(String message) { - String regex = "(https?:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"; - Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.COMMENTS | Pattern.MULTILINE); - Matcher matcher = pattern.matcher(message); - String result = matcher.replaceAll("[$1]"); - - HyperlinkLabel hyperLinkLabel = new HyperlinkLabel(result); + HyperlinkLabel hyperLinkLabel = new HyperlinkLabel(detectUri(message)); hyperLinkLabel.getStyleClass().add("text"); - hyperLinkLabel.setOnAction(event -> { - Hyperlink link = (Hyperlink) event.getSource(); - final String uri = link == null ? "" : link.getText(); - new Thread(() -> browserOpener.accept(uri)).start(); - }); + hyperLinkLabel.setOnAction(this::openUriInBrowser); TextFlow node = new TextFlow(hyperLinkLabel); node.getStyleClass().add(STYLE_CLASS); return node; } + private void openUriInBrowser(ActionEvent event) { + Hyperlink link = (Hyperlink) event.getSource(); + final String uri = link == null ? "" : link.getText(); + new Thread(() -> browserOpener.accept(uri)).start(); + } + + private String detectUri(String message) { + Pattern pattern = Pattern.compile(DETECT_URI, Pattern.CASE_INSENSITIVE | Pattern.COMMENTS | Pattern.MULTILINE); + Matcher matcher = pattern.matcher(message); + return matcher.replaceAll("[$1]"); + } + @NotNull private Label renderLabel(String text) { Label label = new Label(text); From 2e193c708ca19cc63c871632b21612c54ed245ff Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Mon, 17 Oct 2016 11:23:24 +0200 Subject: [PATCH 05/15] fixed tests --- .../ui/actionlog/item/renderer/PlaintextMessageRenderer.java | 3 ++- .../actionlog/item/renderer/PlaintextMessageRendererTest.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index f704d18c..f5d2fbec 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -59,7 +59,8 @@ private String detectUri(String message) { } @NotNull - private Label renderLabel(String text) { + @Deprecated + Label renderLabel(String text) { Label label = new Label(text); label.getStyleClass().add(STYLE_CLASS); return label; diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java index da7bf833..c686f24e 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java @@ -31,7 +31,8 @@ public void rendersPlaintextFromJson() { @Test public void rendersMessageNode() { - Node node = renderer.render(payload, null); + String message = renderer.renderString(payload, null); + Node node = renderer.renderLabel(message); assertTrue(node instanceof Labeled); assertEquals("content", ((Labeled) node).getText()); } From 498a4d6c405433e9e130b1f9471e6b51023e24f3 Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Wed, 19 Oct 2016 11:08:43 +0200 Subject: [PATCH 06/15] fix text wrap --- .../item/renderer/PlaintextMessageRenderer.java | 10 +++------- src/main/resources/main.css | 2 +- .../qabel/desktop/ui/actionlog/ActionlogGuiTest.java | 4 +++- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index f5d2fbec..fcd6ba40 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -5,7 +5,6 @@ import javafx.scene.Node; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; -import javafx.scene.text.TextFlow; import org.controlsfx.control.HyperlinkLabel; import org.jetbrains.annotations.NotNull; @@ -35,15 +34,12 @@ public Node render(String dropPayload, ResourceBundle resourceBundle) { } @NotNull - TextFlow renderHyperlinks(String message) { + HyperlinkLabel renderHyperlinks(String message) { HyperlinkLabel hyperLinkLabel = new HyperlinkLabel(detectUri(message)); hyperLinkLabel.getStyleClass().add("text"); - + hyperLinkLabel.getStyleClass().add(STYLE_CLASS); hyperLinkLabel.setOnAction(this::openUriInBrowser); - - TextFlow node = new TextFlow(hyperLinkLabel); - node.getStyleClass().add(STYLE_CLASS); - return node; + return hyperLinkLabel; } private void openUriInBrowser(ActionEvent event) { diff --git a/src/main/resources/main.css b/src/main/resources/main.css index 656e1e20..869f198a 100644 --- a/src/main/resources/main.css +++ b/src/main/resources/main.css @@ -293,7 +293,6 @@ .message-text { -fx-padding: 1em 1em 1em 1em; -fx-text-alignment: justify; - -fx-wrap-text: true; -fx-text-fill: white; -fx-fill: white; } @@ -305,6 +304,7 @@ -fx-wrap-text: true; -fx-text-fill: white; -fx-fill: white; + -fx-wrap-text: true; } .own .message-text .label, diff --git a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java index 44da0086..94972a20 100644 --- a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java +++ b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java @@ -13,6 +13,7 @@ import java.util.Date; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static junit.framework.TestCase.assertEquals; @@ -38,6 +39,7 @@ public void typeMessageWithHyperlinkAndClickIt() throws Exception { fxMessageRendererFactory.setFallbackRenderer(plaintextMessageRenderer); clickOn(".hyperlink"); assertThat(browserOpener.get(), is("http://qabel.de")); + robot.sleep(5, TimeUnit.SECONDS); } private void submitChat() { @@ -48,7 +50,7 @@ private void submitChat() { private void writeTwoLinesWithOneHyperlink() { FxRobot textArea = clickOn("#textarea"); - textArea.write("line1"); + textArea.write("Minions ipsum pepete underweaaar daa hahaha potatoooo tatata bala tu ti aamoo! Aaaaaah bappleees chasy. Potatoooo tulaliloo potatoooo chasy bananaaaa ti aamoo!"); robot.press(KeyCode.SHIFT); try { robot.push(KeyCode.ENTER); From 13bcba143c9576f66ab978f95ac9741b0f9db725 Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Wed, 19 Oct 2016 11:24:33 +0200 Subject: [PATCH 07/15] test fix to changed implementation --- .../item/renderer/PlaintextMessageRendererTest.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java index c686f24e..d7c949f2 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java @@ -1,9 +1,7 @@ package de.qabel.desktop.ui.actionlog.item.renderer; import de.qabel.desktop.ui.AbstractFxTest; -import javafx.scene.Node; import javafx.scene.control.Labeled; -import javafx.scene.text.TextFlow; import org.controlsfx.control.HyperlinkLabel; import org.junit.Before; import org.junit.Test; @@ -32,8 +30,7 @@ public void rendersPlaintextFromJson() { @Test public void rendersMessageNode() { String message = renderer.renderString(payload, null); - Node node = renderer.renderLabel(message); - assertTrue(node instanceof Labeled); + Labeled node = renderer.renderLabel(message); assertEquals("content", ((Labeled) node).getText()); } @@ -47,9 +44,8 @@ public void renderHyperlinks() throws Exception { AtomicReference browserOpener = new AtomicReference<>(); renderer.browserOpener = browserOpener::set; - Node node = renderer.renderHyperlinks(string); - HyperlinkLabel hyperLinkLabel = ((HyperlinkLabel) ((TextFlow) node).getChildren().get(0)); - assertTrue(hyperLinkLabel.getText().contains(expectedUriFormat)); + HyperlinkLabel node = renderer.renderHyperlinks(string); + assertTrue(node.getText().contains(expectedUriFormat)); } From a21431dc6cd798c6ed8690aaa16c385b072fa7c0 Mon Sep 17 00:00:00 2001 From: nnice Date: Fri, 21 Oct 2016 15:04:01 +0200 Subject: [PATCH 08/15] replace the old actionlogitems with a new one the "old" functionality is present --- .../ui/actionlog/ActionlogController.java | 25 +++--- .../item/NewActionlogItemController.java | 81 +++++++++++++++++++ .../actionlog/item/NewActionlogItemView.java | 11 +++ .../renderer/PlaintextMessageRenderer.java | 1 + .../ui/actionlog/item/myactionlogitem.fxml | 2 +- .../ui/actionlog/item/newactionlogitem.fxml | 38 +++++++++ src/main/resources/main.css | 51 +----------- .../ui/actionlog/ActionlogGuiTest.java | 1 - 8 files changed, 149 insertions(+), 61 deletions(-) create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java create mode 100644 src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java index 0ea859b8..280e3b9a 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java @@ -12,10 +12,7 @@ import de.qabel.desktop.daemon.drop.TextMessage; import de.qabel.desktop.repository.DropMessageRepository; import de.qabel.desktop.ui.AbstractController; -import de.qabel.desktop.ui.actionlog.item.ActionlogItem; -import de.qabel.desktop.ui.actionlog.item.ActionlogItemView; -import de.qabel.desktop.ui.actionlog.item.MyActionlogItemView; -import de.qabel.desktop.ui.actionlog.item.OtherActionlogItemView; +import de.qabel.desktop.ui.actionlog.item.*; import de.qabel.desktop.ui.connector.DropConnector; import javafx.application.Platform; import javafx.fxml.FXML; @@ -178,7 +175,11 @@ private void addMessagesToView(List dropMessages) { for (PersistenceDropMessage d : dropMessages) { Platform.runLater(() -> { if (d.isSent()) { - addOwnMessageToActionlog(d.getDropMessage()); + try { + addOwnMessageToActionlog(d.getDropMessage()); + } catch (EntityNotFoundException e) { + logger.warn("failed to show message: " + e.getMessage(), e); + } } else { try { addMessageToActionlog(d.getDropMessage()); @@ -213,29 +214,31 @@ void addMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundExcepti } Contact sender = contactRepository.findByKeyId(identity, senderKeyId); - if(sender == null){ + if (sender == null) { sender = contactRepository.findByKeyId(identity, dropMessage.getSender().getKeyIdentifier()); } injectionContext.put("dropMessage", dropMessage); - injectionContext.put("contact", sender); - OtherActionlogItemView otherItemView = new OtherActionlogItemView(injectionContext::get); + injectionContext.put("sender", sender.getAlias()); + NewActionlogItemView otherItemView = new NewActionlogItemView(injectionContext::get); messages.getChildren().add(otherItemView.getView()); messageView.add(otherItemView); messageControllers.add((ActionlogItem) otherItemView.getPresenter()); } - void addOwnMessageToActionlog(DropMessage dropMessage) { + void addOwnMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundException { if (dropMessage.getDropPayload().equals("")) { return; } Map injectionContext = new HashMap<>(); injectionContext.put("dropMessage", dropMessage); - MyActionlogItemView myItemView = new MyActionlogItemView(injectionContext::get); + injectionContext.put("sender", identity.getAlias()); + + NewActionlogItemView myItemView = new NewActionlogItemView(injectionContext::get); + messages.getChildren().add(myItemView.getView()); messageView.add(myItemView); messageControllers.add((ActionlogItem) myItemView.getPresenter()); - } void setText(String text) { diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java new file mode 100644 index 00000000..7477f621 --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java @@ -0,0 +1,81 @@ +package de.qabel.desktop.ui.actionlog.item; + +import de.qabel.core.config.Contact; +import de.qabel.core.config.Identity; +import de.qabel.core.drop.DropMessage; +import de.qabel.desktop.ui.AbstractController; +import de.qabel.desktop.ui.actionlog.item.renderer.FXMessageRenderer; +import de.qabel.desktop.ui.actionlog.item.renderer.FXMessageRendererFactory; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import org.controlsfx.control.HyperlinkLabel; +import org.ocpsoft.prettytime.PrettyTime; + +import javax.inject.Inject; +import java.net.URL; +import java.util.ResourceBundle; + +public class NewActionlogItemController extends AbstractController implements Initializable, ActionlogItem { + + @FXML + Label senderName; + + @FXML + TextFlow messageContainer; + + @FXML + Label dateLabel; + + @Inject + String sender; + @Inject + private DropMessage dropMessage; + @Inject + FXMessageRendererFactory messageRendererFactory; + + private PrettyTime p; + + @Override + public void initialize(URL location, ResourceBundle resources) { + messageContainer.getChildren().clear(); + + p = new PrettyTime(resources.getLocale()); + dateLabel.setText(p.format(dropMessage.getCreationDate())); + + senderName.setText(sender); + + FXMessageRenderer renderer = messageRendererFactory.getRenderer(dropMessage.getDropPayloadType()); + Node renderedMessage = renderer.render(dropMessage.getDropPayload(), resources); + renderedMessage.getStyleClass().add("sent"); + messageContainer.getChildren().addAll(renderedMessage); + + + } + + @Override + public void refreshDate() { + Platform.runLater(() -> dateLabel.setText(p.format(dropMessage.getCreationDate()))); + } + + public Label getDateLabel() { + return dateLabel; + } + + public void setDateLabel(Label dateLabel) { + this.dateLabel = dateLabel; + } + + public DropMessage getDropMessage() { + return dropMessage; + } + + public void setDropMessage(DropMessage dropMessage) { + this.dropMessage = dropMessage; + } +} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java new file mode 100644 index 00000000..5bca19a7 --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java @@ -0,0 +1,11 @@ +package de.qabel.desktop.ui.actionlog.item; + +import com.airhacks.afterburner.views.QabelFXMLView; + +import java.util.function.Function; + +public class NewActionlogItemView extends QabelFXMLView implements ActionlogItemView{ + public NewActionlogItemView(Function injectionContext) { + super(injectionContext); + } +} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index fcd6ba40..02bac91d 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -5,6 +5,7 @@ import javafx.scene.Node; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; +import javafx.scene.text.Text; import org.controlsfx.control.HyperlinkLabel; import org.jetbrains.annotations.NotNull; diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml index cac15b6c..ef7ef7ca 100644 --- a/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml +++ b/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml @@ -7,7 +7,7 @@ + fx:controller="de.qabel.desktop.ui.actionlog.item.MyActionlogItemController" fx:id="messageItem"> diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml new file mode 100644 index 00000000..7177783a --- /dev/null +++ b/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + +
+ + + + +
+ + + +
+
+
+ + + +
diff --git a/src/main/resources/main.css b/src/main/resources/main.css index 869f198a..2965fe05 100644 --- a/src/main/resources/main.css +++ b/src/main/resources/main.css @@ -150,15 +150,6 @@ -fx-background-color: transparent; } -#otherMessagebox { - -fx-background-color: #ff690f; - -fx-background-radius: 5; -} - -#myMessagebox { - -fx-background-color: #4d4d4d; - -fx-background-radius: 5; -} #shadow { -fx-effect: dropshadow(one-pass-box, grey, 5, -1, 0, 2); } @@ -264,11 +255,9 @@ .hyperlink .text { -fx-fill: #222222; } - .hyperlink:visited .text { -fx-fill: #222222; } - .hyperlink:hover .text { -fx-fill: #787878; } @@ -283,49 +272,15 @@ -fx-text-alignment: justify; -fx-wrap-text: true; } - +.message-sendername { + -fx-font-weight: bold; +} .own .message-date { -fx-alignment: bottom-right; -fx-text-alignment: right; } -.message-text { - -fx-padding: 1em 1em 1em 1em; - -fx-text-alignment: justify; - -fx-text-fill: white; - -fx-fill: white; -} - -.message-text, -.message-text .label, -.message-text .text, -.message-text Text { - -fx-wrap-text: true; - -fx-text-fill: white; - -fx-fill: white; - -fx-wrap-text: true; -} - -.own .message-text .label, -.own .message-text Text { - -fx-text-alignment: right; - -fx-text-fill: #4d4d4d; -} - -.other .message-text .label , -.other .message-text Text { - -fx-text-alignment: left; - -} - -.own .message-text .hyperlink .text { - -fx-fill: #ff690f; -} - -.other .message-text .hyperlink .text { - -fx-fill: #4d4d4d; -} .payload-type-icon { -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.6), 2, 0.0, 0, 2); diff --git a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java index 94972a20..edfbd030 100644 --- a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java +++ b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java @@ -39,7 +39,6 @@ public void typeMessageWithHyperlinkAndClickIt() throws Exception { fxMessageRendererFactory.setFallbackRenderer(plaintextMessageRenderer); clickOn(".hyperlink"); assertThat(browserOpener.get(), is("http://qabel.de")); - robot.sleep(5, TimeUnit.SECONDS); } private void submitChat() { From 14a03d69c26611a4ef4ece241715b5306a93c70a Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Mon, 24 Oct 2016 15:39:18 +0200 Subject: [PATCH 09/15] add own QabelChatLabel and Skin and made all childs into an Text object to simplify the text flow wrapping stuff --- .../renderer/PlaintextMessageRenderer.java | 21 +++-- .../ui/actionlog/util/QabelChatLabel.java | 20 +++++ .../ui/actionlog/util/QabelChatSkin.java | 87 +++++++++++++++++++ .../ui/actionlog/ActionlogGuiTest.java | 21 +++-- 4 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index 02bac91d..4d75faba 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -1,12 +1,10 @@ package de.qabel.desktop.ui.actionlog.item.renderer; import de.qabel.desktop.daemon.drop.TextMessage; -import javafx.event.ActionEvent; +import de.qabel.desktop.ui.actionlog.util.QabelChatLabel; import javafx.scene.Node; -import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.text.Text; -import org.controlsfx.control.HyperlinkLabel; import org.jetbrains.annotations.NotNull; import java.awt.*; @@ -35,18 +33,19 @@ public Node render(String dropPayload, ResourceBundle resourceBundle) { } @NotNull - HyperlinkLabel renderHyperlinks(String message) { - HyperlinkLabel hyperLinkLabel = new HyperlinkLabel(detectUri(message)); - hyperLinkLabel.getStyleClass().add("text"); - hyperLinkLabel.getStyleClass().add(STYLE_CLASS); - hyperLinkLabel.setOnAction(this::openUriInBrowser); - return hyperLinkLabel; + QabelChatLabel renderHyperlinks(String message) { + QabelChatLabel node = new QabelChatLabel(detectUri(message)); + node.getStyleClass().add("text"); + node.getStyleClass().add(STYLE_CLASS); + node.setOnMouseClicked(this::openUriInBrowser); + return node; } - private void openUriInBrowser(ActionEvent event) { - Hyperlink link = (Hyperlink) event.getSource(); + private void openUriInBrowser(javafx.event.Event event) { + Text link = (Text) event.getSource(); final String uri = link == null ? "" : link.getText(); new Thread(() -> browserOpener.accept(uri)).start(); + event.consume(); } private String detectUri(String message) { diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java new file mode 100644 index 00000000..e6082f1b --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java @@ -0,0 +1,20 @@ +package de.qabel.desktop.ui.actionlog.util; + +import javafx.scene.control.Skin; +import org.controlsfx.control.HyperlinkLabel; + +public class QabelChatLabel extends HyperlinkLabel { + + public QabelChatLabel() { + this(null); + } + + public QabelChatLabel(String text) { + super(text); + } + + @Override + protected Skin createDefaultSkin() { + return new QabelChatSkin(this); + } +} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java new file mode 100644 index 00000000..4ea4cdc0 --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java @@ -0,0 +1,87 @@ +package de.qabel.desktop.ui.actionlog.util; + +import com.sun.javafx.scene.control.behavior.BehaviorBase; +import com.sun.javafx.scene.control.skin.BehaviorSkinBase; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class QabelChatSkin extends BehaviorSkinBase> { + + // The strings used to delimit the hyperlinks + private static final String HYPERLINK_START = "["; //$NON-NLS-1$ + private static final String HYPERLINK_END = "]"; //$NON-NLS-1$ + private TextFlow textFlow; + + protected QabelChatSkin(QabelChatLabel control) { + super(control, new BehaviorBase<>(control, Collections.emptyList())); + + textFlow = new TextFlow(); + textFlow.setMaxWidth(Control.USE_PREF_SIZE); + textFlow.setMaxHeight(Control.USE_PREF_SIZE); + + getChildren().add(textFlow); + updateText(); + + registerChangeListener(control.textProperty(), "TEXT"); //$NON-NLS-1$ + } + + + @Override + protected void handleControlPropertyChanged(String p) { + super.handleControlPropertyChanged(p); + + if (p.equals("TEXT")) { //$NON-NLS-1$ + updateText(); + } + } + + private void updateText() { + final String text = getSkinnable().getText(); + + if (text == null || text.isEmpty()) { + textFlow.getChildren().clear(); + return; + } + + // parse the text and put it into an array list + final List nodes = new ArrayList<>(); + + int start = 0; + final int textLength = text.length(); + while (start != -1 && start < textLength) { + int startPos = text.indexOf(HYPERLINK_START, start); + int endPos = text.indexOf(HYPERLINK_END, startPos); + + // if the startPos is -1, there are no more hyperlinks... + if (startPos == -1 || endPos == -1) { + if (textLength > start) { + // ...but there is still text to turn into one last label + Text label = new Text(text.substring(start)); + nodes.add(label); + break; + } + } + + // firstly, create a label from start to startPos + Text label = new Text(text.substring(start, startPos)); + nodes.add(label); + + // if endPos is greater than startPos, create a hyperlink + Text hyperlink = new Text(text.substring(startPos + 1, endPos)); + hyperlink.getStyleClass().add("hyperlink"); + hyperlink.onMouseClickedProperty().bind(getSkinnable().onMouseClickedProperty()); + nodes.add(hyperlink); + + start = endPos + 1; + } + + textFlow.getChildren().setAll(nodes); + + } +} diff --git a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java index edfbd030..b178f93c 100644 --- a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java +++ b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java @@ -13,12 +13,13 @@ import java.util.Date; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static junit.framework.TestCase.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class ActionlogGuiTest extends AbstractGuiTest { @@ -48,15 +49,13 @@ private void submitChat() { } private void writeTwoLinesWithOneHyperlink() { - FxRobot textArea = clickOn("#textarea"); - textArea.write("Minions ipsum pepete underweaaar daa hahaha potatoooo tatata bala tu ti aamoo! Aaaaaah bappleees chasy. Potatoooo tulaliloo potatoooo chasy bananaaaa ti aamoo!"); - robot.press(KeyCode.SHIFT); - try { - robot.push(KeyCode.ENTER); - } finally { - robot.release(KeyCode.SHIFT); - } - robot.write("http://qabel.de"); + waitUntil(() -> controller.textarea != null); + clickOn("#textarea"); + controller.textarea.setText( + "Minions ipsum pepete underweaaar daa hahaha potatoooo tatata bala tu ti aamoo! Aaaaaah bappleees chasy. Potatoooo tulaliloo potatoooo chasy bananaaaa ti aamoo!\n" + + "http://qabel.de" + ); + } @Test From 065ecf94d41f8fbe69b00566d0358aaeae384db8 Mon Sep 17 00:00:00 2001 From: nerminnicevic Date: Tue, 25 Oct 2016 16:11:06 +0200 Subject: [PATCH 10/15] some refactoring and cleanup add the alias into the textflow some text wrap wont work gonna check on it later fully replaced old chat stuff --- .../de/qabel/desktop/DesktopClientGui.java | 2 + .../BoxObjectCellValueFactory.java | 56 --------------- .../ui/actionlog/ActionlogController.java | 6 +- .../item/MyActionlogItemController.java | 65 ------------------ .../actionlog/item/MyActionlogItemView.java | 11 --- .../item/NewActionlogItemController.java | 21 +++--- .../item/OtherActionlogItemController.java | 66 ------------------ .../item/OtherActionlogItemView.java | 11 --- .../item/renderer/FXMessageRenderer.java | 2 +- .../renderer/PlaintextMessageRenderer.java | 8 +-- .../renderer/ShareNotificationRenderer.java | 2 +- .../ui/actionlog/util/QabelChatLabel.java | 9 ++- .../ui/actionlog/util/QabelChatSkin.java | 54 ++++++++------- .../qabel/desktop/ui/actionlog/actionlog.fxml | 62 ++++++++--------- .../ui/actionlog/item/myactionlogitem.css | 0 .../ui/actionlog/item/myactionlogitem.fxml | 58 ---------------- .../ui/actionlog/item/newactionlogitem.fxml | 31 ++++----- .../ui/actionlog/item/otheractionlogitem.css | 0 .../ui/actionlog/item/otheractionlogitem.fxml | 68 ------------------- src/main/resources/main.css | 12 +--- .../ui/actionlog/ActionlogControllerTest.java | 8 +-- .../PlaintextMessageRendererTest.java | 2 +- 22 files changed, 103 insertions(+), 451 deletions(-) delete mode 100644 src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java delete mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemController.java delete mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemView.java delete mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemController.java delete mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemView.java delete mode 100644 src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.css delete mode 100644 src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml delete mode 100644 src/main/resources/de/qabel/desktop/ui/actionlog/item/otheractionlogitem.css delete mode 100644 src/main/resources/de/qabel/desktop/ui/actionlog/item/otheractionlogitem.fxml diff --git a/src/main/java/de/qabel/desktop/DesktopClientGui.java b/src/main/java/de/qabel/desktop/DesktopClientGui.java index 29b2b831..d9bc0f54 100644 --- a/src/main/java/de/qabel/desktop/DesktopClientGui.java +++ b/src/main/java/de/qabel/desktop/DesktopClientGui.java @@ -26,6 +26,7 @@ import javafx.scene.image.Image; import javafx.stage.Stage; import org.jetbrains.annotations.NotNull; +import org.scenicview.ScenicView; import java.awt.*; import java.io.IOException; @@ -78,6 +79,7 @@ public void run() { }); trayNotifications(tray); + ScenicView.show(primaryStage.getScene()); } private void setUpWindow() { diff --git a/src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java b/src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java deleted file mode 100644 index e99fa0a3..00000000 --- a/src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.qabel.desktop.cellValueFactory; - -import de.qabel.box.storage.BoxFile; -import de.qabel.box.storage.BoxObject; -import de.qabel.desktop.ui.remotefs.FolderTreeItem; -import javafx.beans.property.ReadOnlyStringWrapper; -import javafx.beans.value.ObservableValue; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeTableColumn; -import javafx.util.Callback; -import org.apache.commons.io.FileUtils; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; - - -public class BoxObjectCellValueFactory implements Callback, ObservableValue> { - public static final String SIZE = "size"; - public static final String MTIME = "mtime"; - public static final String NAME = "name"; - - private String searchValue; - - public BoxObjectCellValueFactory(String searchValue) { - this.searchValue = searchValue; - } - - @Override - public ObservableValue call(TreeTableColumn.CellDataFeatures p) { - TreeItem treeItem = p.getValue(); - BoxObject bf = treeItem.getValue(); - - if (bf == null) { - return new ReadOnlyStringWrapper("-"); - } - - if (searchValue.equals(NAME)) { - if (treeItem instanceof FolderTreeItem) { - return ((FolderTreeItem) treeItem).getNameProperty(); - } - return new ReadOnlyStringWrapper(bf.getName()); - } - - if (bf instanceof BoxFile) { - switch (searchValue) { - case SIZE: - String formattedFileSize = FileUtils.byteCountToDisplaySize(((BoxFile) bf).getSize()); - return new ReadOnlyStringWrapper(formattedFileSize); - case MTIME: - DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); - return new ReadOnlyStringWrapper(dateFormat.format(((BoxFile) bf).getMtime())); - } - } - return new ReadOnlyStringWrapper(""); - } -} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java index 280e3b9a..a422d2f5 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java @@ -17,10 +17,7 @@ import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.TextArea; +import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; @@ -93,6 +90,7 @@ public void initialize(URL location, ResourceBundle resources) { clientConfiguration.onSelectIdentity(identity -> this.identity = identity); addListener(); contactRepository.attach(this::toggleNotification); + scroller.setFitToWidth(true); } private void toggleNotification() { diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemController.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemController.java deleted file mode 100644 index 1da90302..00000000 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemController.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.qabel.desktop.ui.actionlog.item; - -import de.qabel.core.drop.DropMessage; -import de.qabel.desktop.ui.AbstractController; -import de.qabel.desktop.ui.actionlog.item.renderer.FXMessageRenderer; -import de.qabel.desktop.ui.actionlog.item.renderer.FXMessageRendererFactory; -import javafx.application.Platform; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.Pane; -import org.ocpsoft.prettytime.PrettyTime; - -import javax.inject.Inject; -import java.net.URL; -import java.util.ResourceBundle; - -public class MyActionlogItemController extends AbstractController implements Initializable, ActionlogItem { - @FXML - Pane messageContainer; - @FXML - Label dateLabel; - - @Inject - private DropMessage dropMessage; - @Inject - FXMessageRendererFactory messageRendererFactory; - - PrettyTime p; - - @Override - public void initialize(URL location, ResourceBundle resources) { - FXMessageRenderer renderer = messageRendererFactory.getRenderer(dropMessage.getDropPayloadType()); - Node renderedMessage = renderer.render(dropMessage.getDropPayload(), resources); - renderedMessage.getStyleClass().add("sent"); - messageContainer.getChildren().addAll(renderedMessage); - - p = new PrettyTime(resources.getLocale()); - dateLabel.setText(p.format(dropMessage.getCreationDate())); - } - - - @Override - public void refreshDate() { - Platform.runLater(()-> dateLabel.setText(p.format(dropMessage.getCreationDate()))); - } - - - public Label getDateLabel() { - return dateLabel; - } - - public void setDateLabel(Label dateLabel) { - this.dateLabel = dateLabel; - } - - public DropMessage getDropMessage() { - return dropMessage; - } - - public void setDropMessage(DropMessage dropMessage) { - this.dropMessage = dropMessage; - } -} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemView.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemView.java deleted file mode 100644 index 18177a09..00000000 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/MyActionlogItemView.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.qabel.desktop.ui.actionlog.item; - -import com.airhacks.afterburner.views.QabelFXMLView; - -import java.util.function.Function; - -public class MyActionlogItemView extends QabelFXMLView implements ActionlogItemView{ - public MyActionlogItemView(Function injectionContext) { - super(injectionContext); - } -} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java index 7477f621..1b6114f5 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java @@ -1,7 +1,5 @@ package de.qabel.desktop.ui.actionlog.item; -import de.qabel.core.config.Contact; -import de.qabel.core.config.Identity; import de.qabel.core.drop.DropMessage; import de.qabel.desktop.ui.AbstractController; import de.qabel.desktop.ui.actionlog.item.renderer.FXMessageRenderer; @@ -11,10 +9,8 @@ import javafx.fxml.Initializable; import javafx.scene.Node; import javafx.scene.control.Label; -import javafx.scene.layout.Pane; -import javafx.scene.text.Text; import javafx.scene.text.TextFlow; -import org.controlsfx.control.HyperlinkLabel; +import org.jetbrains.annotations.NotNull; import org.ocpsoft.prettytime.PrettyTime; import javax.inject.Inject; @@ -23,9 +19,6 @@ public class NewActionlogItemController extends AbstractController implements Initializable, ActionlogItem { - @FXML - Label senderName; - @FXML TextFlow messageContainer; @@ -48,14 +41,16 @@ public void initialize(URL location, ResourceBundle resources) { p = new PrettyTime(resources.getLocale()); dateLabel.setText(p.format(dropMessage.getCreationDate())); - senderName.setText(sender); + Node renderedMessage = getRenderedMessage(sender, resources); + messageContainer.getChildren().addAll(renderedMessage); + } + @NotNull + private Node getRenderedMessage(String prefixAlias, ResourceBundle resources) { FXMessageRenderer renderer = messageRendererFactory.getRenderer(dropMessage.getDropPayloadType()); - Node renderedMessage = renderer.render(dropMessage.getDropPayload(), resources); + Node renderedMessage = renderer.render(prefixAlias, dropMessage.getDropPayload(), resources); renderedMessage.getStyleClass().add("sent"); - messageContainer.getChildren().addAll(renderedMessage); - - + return renderedMessage; } @Override diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemController.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemController.java deleted file mode 100644 index 4110a00c..00000000 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemController.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.qabel.desktop.ui.actionlog.item; - -import de.qabel.core.config.Contact; -import de.qabel.core.drop.DropMessage; -import de.qabel.desktop.ui.AbstractController; -import de.qabel.desktop.ui.accounting.avatar.AvatarView; -import de.qabel.desktop.ui.actionlog.item.renderer.FXMessageRenderer; -import de.qabel.desktop.ui.actionlog.item.renderer.FXMessageRendererFactory; -import javafx.application.Platform; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.Pane; -import javafx.scene.text.TextAlignment; -import org.ocpsoft.prettytime.PrettyTime; - -import javax.inject.Inject; -import java.net.URL; -import java.util.ResourceBundle; - -public class OtherActionlogItemController extends AbstractController implements Initializable, ActionlogItem { - ResourceBundle resourceBundle; - - @FXML - Label dateLabel; - @FXML - Pane avatarContainer; - @FXML - Pane messageContainer; - - @Inject - DropMessage dropMessage; - @Inject - Contact contact; - @Inject - FXMessageRendererFactory messageRendererFactory; - - PrettyTime p; - - @Override - public void initialize(URL location, ResourceBundle resources) { - - FXMessageRenderer renderer = messageRendererFactory.getRenderer(dropMessage.getDropPayloadType()); - Node renderedMessage = renderer.render(dropMessage.getDropPayload(), resources); - messageContainer.getChildren().addAll(renderedMessage); - - p = new PrettyTime(resources.getLocale()); - dateLabel.setText(p.format(dropMessage.getCreationDate())); - - dateLabel.setWrapText(true); - - dateLabel.setTextAlignment(TextAlignment.JUSTIFY); - updateAvatar(); - } - - private void updateAvatar() { - new AvatarView(contact.getAlias()).place(avatarContainer); - } - - - @Override - public void refreshDate() { - Platform.runLater(()-> dateLabel.setText(p.format(dropMessage.getCreationDate()))); - } -} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemView.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemView.java deleted file mode 100644 index 79e5bd8f..00000000 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/OtherActionlogItemView.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.qabel.desktop.ui.actionlog.item; - -import com.airhacks.afterburner.views.QabelFXMLView; - -import java.util.function.Function; - -public class OtherActionlogItemView extends QabelFXMLView implements ActionlogItemView{ - public OtherActionlogItemView(Function injectionContext) { - super(injectionContext); - } -} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/FXMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/FXMessageRenderer.java index fab44a93..a2bfedd8 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/FXMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/FXMessageRenderer.java @@ -5,5 +5,5 @@ import java.util.ResourceBundle; public interface FXMessageRenderer extends MessageRenderer { - Node render(String dropPayload, ResourceBundle resourceBundle); + Node render(String prefixAlias, String dropPayload, ResourceBundle resourceBundle); } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index 4d75faba..baec1c47 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -27,14 +27,14 @@ public class PlaintextMessageRenderer implements FXMessageRenderer { private static final String DETECT_URI = "(https?:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"; @Override - public Node render(String dropPayload, ResourceBundle resourceBundle) { + public Node render(String prefixAlias, String dropPayload, ResourceBundle resourceBundle) { String text = renderString(dropPayload, resourceBundle); - return renderHyperlinks(text); + return renderTextFlow(prefixAlias, text); } @NotNull - QabelChatLabel renderHyperlinks(String message) { - QabelChatLabel node = new QabelChatLabel(detectUri(message)); + QabelChatLabel renderTextFlow(String prefixAlias, String message) { + QabelChatLabel node = new QabelChatLabel(prefixAlias, detectUri(message)); node.getStyleClass().add("text"); node.getStyleClass().add(STYLE_CLASS); node.setOnMouseClicked(this::openUriInBrowser); diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java index a4221d9a..3886fab6 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java @@ -28,7 +28,7 @@ public ShareNotificationRenderer(AuthenticatedDownloader downloader, SharingServ } @Override - public Node render(String dropPayload, ResourceBundle resourceBundle) { + public Node render(String prefixAlias, String dropPayload, ResourceBundle resourceBundle) { VBox result = new VBox(); result.getStyleClass().add("message-text"); result.setStyle("-fx-spacing: 1em;"); diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java index e6082f1b..95194717 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java @@ -5,16 +5,15 @@ public class QabelChatLabel extends HyperlinkLabel { - public QabelChatLabel() { - this(null); - } + private String prefixAlias; - public QabelChatLabel(String text) { + public QabelChatLabel(String prefixAlias, String text) { super(text); + this.prefixAlias = prefixAlias; } @Override protected Skin createDefaultSkin() { - return new QabelChatSkin(this); + return new QabelChatSkin(prefixAlias, this); } } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java index 4ea4cdc0..73715544 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java @@ -11,27 +11,28 @@ import java.util.Collections; import java.util.List; -public class QabelChatSkin extends BehaviorSkinBase> { +class QabelChatSkin extends BehaviorSkinBase> { // The strings used to delimit the hyperlinks private static final String HYPERLINK_START = "["; //$NON-NLS-1$ private static final String HYPERLINK_END = "]"; //$NON-NLS-1$ private TextFlow textFlow; + private final Text senderAlias; - protected QabelChatSkin(QabelChatLabel control) { + QabelChatSkin(String senderAlias, QabelChatLabel control) { super(control, new BehaviorBase<>(control, Collections.emptyList())); textFlow = new TextFlow(); - textFlow.setMaxWidth(Control.USE_PREF_SIZE); - textFlow.setMaxHeight(Control.USE_PREF_SIZE); - getChildren().add(textFlow); + this.senderAlias = new Text(senderAlias + " "); + this.senderAlias.getStyleClass().add("message-sendername"); + + getChildren().addAll(textFlow); updateText(); registerChangeListener(control.textProperty(), "TEXT"); //$NON-NLS-1$ } - @Override protected void handleControlPropertyChanged(String p) { super.handleControlPropertyChanged(p); @@ -43,45 +44,50 @@ protected void handleControlPropertyChanged(String p) { private void updateText() { final String text = getSkinnable().getText(); - if (text == null || text.isEmpty()) { textFlow.getChildren().clear(); return; } - // parse the text and put it into an array list final List nodes = new ArrayList<>(); + nodes.add(senderAlias); + int start = 0; final int textLength = text.length(); while (start != -1 && start < textLength) { int startPos = text.indexOf(HYPERLINK_START, start); int endPos = text.indexOf(HYPERLINK_END, startPos); - // if the startPos is -1, there are no more hyperlinks... - if (startPos == -1 || endPos == -1) { + if (isNotHyperlink(startPos, endPos)) { if (textLength > start) { // ...but there is still text to turn into one last label - Text label = new Text(text.substring(start)); - nodes.add(label); + appendTextNode(nodes, text.substring(start)); break; } } - - // firstly, create a label from start to startPos - Text label = new Text(text.substring(start, startPos)); - nodes.add(label); - - // if endPos is greater than startPos, create a hyperlink - Text hyperlink = new Text(text.substring(startPos + 1, endPos)); - hyperlink.getStyleClass().add("hyperlink"); - hyperlink.onMouseClickedProperty().bind(getSkinnable().onMouseClickedProperty()); - nodes.add(hyperlink); - + appendTextNode(nodes, text.substring(start, startPos)); + appendHyperlink(nodes, text.substring(startPos + 1, endPos)); start = endPos + 1; } - + textFlow.setMaxWidth(Control.USE_PREF_SIZE); textFlow.getChildren().setAll(nodes); + textFlow.requestLayout(); + } + + private boolean isNotHyperlink(int startPos, int endPos) { + return startPos == -1 || endPos == -1; + } + + private void appendTextNode(List nodes, String content) { + Text textnode = new Text(content); + nodes.add(textnode); + } + private void appendHyperlink(List nodes, String content) { + Text hyperlink = new Text(content); + hyperlink.getStyleClass().add("hyperlink"); + hyperlink.onMouseClickedProperty().bind(getSkinnable().onMouseClickedProperty()); + nodes.add(hyperlink); } } diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/actionlog.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/actionlog.fxml index c4639a22..a1328f79 100644 --- a/src/main/resources/de/qabel/desktop/ui/actionlog/actionlog.fxml +++ b/src/main/resources/de/qabel/desktop/ui/actionlog/actionlog.fxml @@ -9,44 +9,41 @@ - - - -
-
- - - - - - -
-
-
+ + + +
+
+ + + + + + +
+
+
- - - - - + + - +
@@ -58,7 +55,8 @@ -
diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.css b/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml deleted file mode 100644 index ef7ef7ca..00000000 --- a/src/main/resources/de/qabel/desktop/ui/actionlog/item/myactionlogitem.fxml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - -
- - -
- - - - - - -
-
-
- - - - - - - - - - - - - -
- - - -
-
-
-
-
diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml index 7177783a..ea80fb37 100644 --- a/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml +++ b/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml @@ -1,38 +1,33 @@ + + + + - + -
- - - - -
- - - -
-
+ + + + +
- +
diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/otheractionlogitem.css b/src/main/resources/de/qabel/desktop/ui/actionlog/item/otheractionlogitem.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/otheractionlogitem.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/item/otheractionlogitem.fxml deleted file mode 100644 index 5e614f72..00000000 --- a/src/main/resources/de/qabel/desktop/ui/actionlog/item/otheractionlogitem.fxml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - -
- - -
- - - - - - -
- - -
- - - -
-
- - - -
-
- - - - - - - -
-
-
diff --git a/src/main/resources/main.css b/src/main/resources/main.css index 2965fe05..c1ab0b3d 100644 --- a/src/main/resources/main.css +++ b/src/main/resources/main.css @@ -266,19 +266,13 @@ -fx-fill: #fd670d; } - -.message-date { - -fx-padding: 0 1em 0.5em 1em; - -fx-text-alignment: justify; - -fx-wrap-text: true; -} .message-sendername { -fx-font-weight: bold; } -.own .message-date { - -fx-alignment: bottom-right; - -fx-text-alignment: right; +.message-text .hyperlink { + -fx-fill: #ff690f; + -fx-text-fill: #ff690f; } diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java index 42fdad4e..a2ebc4de 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java @@ -11,8 +11,8 @@ import de.qabel.desktop.repository.DropMessageRepository; import de.qabel.desktop.repository.inmemory.InMemoryDropMessageRepository; import de.qabel.desktop.ui.AbstractControllerTest; -import de.qabel.desktop.ui.actionlog.item.MyActionlogItemController; -import de.qabel.desktop.ui.actionlog.item.MyActionlogItemView; +import de.qabel.desktop.ui.actionlog.item.NewActionlogItemController; +import de.qabel.desktop.ui.actionlog.item.NewActionlogItemView; import org.junit.Before; import org.junit.Test; @@ -93,8 +93,8 @@ public void refreshTime() throws Exception { injectionContext.put("dropMessage", d); injectionContext.put("contact", sender); - MyActionlogItemView my = new MyActionlogItemView(injectionContext::get); - MyActionlogItemController messagesController = (MyActionlogItemController) my.getPresenter(); + NewActionlogItemView my = new NewActionlogItemView(injectionContext::get); + NewActionlogItemController messagesController = (NewActionlogItemController) my.getPresenter(); controller.messageControllers.add(messagesController); messagesController.setDropMessage(d); diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java index d7c949f2..be66a859 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java @@ -44,7 +44,7 @@ public void renderHyperlinks() throws Exception { AtomicReference browserOpener = new AtomicReference<>(); renderer.browserOpener = browserOpener::set; - HyperlinkLabel node = renderer.renderHyperlinks(string); + HyperlinkLabel node = renderer.renderTextFlow("", string); assertTrue(node.getText().contains(expectedUriFormat)); } From e451fcec9e117282f1517b07baed34403f7f90e3 Mon Sep 17 00:00:00 2001 From: julianseeger Date: Wed, 26 Oct 2016 19:28:14 +0200 Subject: [PATCH 11/15] resolves #522 clickable links as Text in TextFlow --- .../de/qabel/desktop/DesktopClientGui.java | 4 +- .../BoxObjectCellValueFactory.java | 56 +++++++++++ .../ui/actionlog/ActionlogController.java | 29 ++++-- .../qabel/desktop/ui/actionlog/Hyperlink.java | 41 +++++++++ ...ller.java => ActionlogItemController.java} | 27 ++---- .../ui/actionlog/item/ActionlogItemView.java | 8 +- .../actionlog/item/NewActionlogItemView.java | 11 --- .../renderer/PlaintextMessageRenderer.java | 92 ++++++++++--------- .../renderer/ShareNotificationRenderer.java | 7 +- .../AbstractPatternExtractionRenderer.java | 39 ++++++++ .../renderer/partial/HyperlinkRenderer.java | 45 +++++++++ .../partial/PartialFXMessageRenderer.java | 10 ++ .../ui/actionlog/item/actionlogitem.fxml | 19 ++++ .../ui/actionlog/item/newactionlogitem.fxml | 33 ------- src/main/resources/main.css | 40 +++++++- .../ui/actionlog/ActionlogGuiTest.java | 2 +- .../ui/actionlog/ActionlogControllerTest.java | 8 +- .../PlaintextMessageRendererTest.java | 36 ++++---- .../partial/HyperlinkRendererTest.java | 73 +++++++++++++++ 19 files changed, 435 insertions(+), 145 deletions(-) create mode 100644 src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/Hyperlink.java rename src/main/java/de/qabel/desktop/ui/actionlog/item/{NewActionlogItemController.java => ActionlogItemController.java} (66%) delete mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/PartialFXMessageRenderer.java create mode 100644 src/main/resources/de/qabel/desktop/ui/actionlog/item/actionlogitem.fxml delete mode 100644 src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml create mode 100644 src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java diff --git a/src/main/java/de/qabel/desktop/DesktopClientGui.java b/src/main/java/de/qabel/desktop/DesktopClientGui.java index d9bc0f54..6a68301d 100644 --- a/src/main/java/de/qabel/desktop/DesktopClientGui.java +++ b/src/main/java/de/qabel/desktop/DesktopClientGui.java @@ -26,7 +26,6 @@ import javafx.scene.image.Image; import javafx.stage.Stage; import org.jetbrains.annotations.NotNull; -import org.scenicview.ScenicView; import java.awt.*; import java.io.IOException; @@ -79,7 +78,6 @@ public void run() { }); trayNotifications(tray); - ScenicView.show(primaryStage.getScene()); } private void setUpWindow() { @@ -120,7 +118,7 @@ private void showLayoutStage() { Scene layoutScene = new Scene(view, 900, 600, true, SceneAntialiasing.BALANCED); Platform.runLater(() -> primaryStage.setScene(layoutScene)); primaryStage.show(); - closeStage(); +// closeStage(); } private void closeStage() { diff --git a/src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java b/src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java new file mode 100644 index 00000000..e99fa0a3 --- /dev/null +++ b/src/main/java/de/qabel/desktop/cellValueFactory/BoxObjectCellValueFactory.java @@ -0,0 +1,56 @@ +package de.qabel.desktop.cellValueFactory; + +import de.qabel.box.storage.BoxFile; +import de.qabel.box.storage.BoxObject; +import de.qabel.desktop.ui.remotefs.FolderTreeItem; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeTableColumn; +import javafx.util.Callback; +import org.apache.commons.io.FileUtils; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + + +public class BoxObjectCellValueFactory implements Callback, ObservableValue> { + public static final String SIZE = "size"; + public static final String MTIME = "mtime"; + public static final String NAME = "name"; + + private String searchValue; + + public BoxObjectCellValueFactory(String searchValue) { + this.searchValue = searchValue; + } + + @Override + public ObservableValue call(TreeTableColumn.CellDataFeatures p) { + TreeItem treeItem = p.getValue(); + BoxObject bf = treeItem.getValue(); + + if (bf == null) { + return new ReadOnlyStringWrapper("-"); + } + + if (searchValue.equals(NAME)) { + if (treeItem instanceof FolderTreeItem) { + return ((FolderTreeItem) treeItem).getNameProperty(); + } + return new ReadOnlyStringWrapper(bf.getName()); + } + + if (bf instanceof BoxFile) { + switch (searchValue) { + case SIZE: + String formattedFileSize = FileUtils.byteCountToDisplaySize(((BoxFile) bf).getSize()); + return new ReadOnlyStringWrapper(formattedFileSize); + case MTIME: + DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); + return new ReadOnlyStringWrapper(dateFormat.format(((BoxFile) bf).getMtime())); + } + } + return new ReadOnlyStringWrapper(""); + } +} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java index a422d2f5..0d019815 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java @@ -1,6 +1,7 @@ package de.qabel.desktop.ui.actionlog; import de.qabel.core.config.Contact; +import de.qabel.core.config.Entity; import de.qabel.core.config.Identity; import de.qabel.core.drop.DropMessage; import de.qabel.core.drop.DropMessageMetadata; @@ -12,12 +13,17 @@ import de.qabel.desktop.daemon.drop.TextMessage; import de.qabel.desktop.repository.DropMessageRepository; import de.qabel.desktop.ui.AbstractController; -import de.qabel.desktop.ui.actionlog.item.*; +import de.qabel.desktop.ui.actionlog.item.ActionlogItem; +import de.qabel.desktop.ui.actionlog.item.ActionlogItemView; import de.qabel.desktop.ui.connector.DropConnector; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.*; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextArea; import javafx.scene.input.KeyCode; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; @@ -204,6 +210,7 @@ private void markSeen(PersistenceDropMessage d) { } } + private Entity lastSender; void addMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundException { Map injectionContext = new HashMap<>(); String senderKeyId = dropMessage.getSenderKeyId(); @@ -215,10 +222,15 @@ void addMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundExcepti if (sender == null) { sender = contactRepository.findByKeyId(identity, dropMessage.getSender().getKeyIdentifier()); } + boolean first = lastSender != sender; + lastSender = sender; injectionContext.put("dropMessage", dropMessage); injectionContext.put("sender", sender.getAlias()); - NewActionlogItemView otherItemView = new NewActionlogItemView(injectionContext::get); - messages.getChildren().add(otherItemView.getView()); + ActionlogItemView otherItemView = new ActionlogItemView(injectionContext::get); + Parent view = otherItemView.getView(); + view.getStyleClass().add("sent"); + view.getStyleClass().add(first ? "first" : "sequence"); + messages.getChildren().add(view); messageView.add(otherItemView); messageControllers.add((ActionlogItem) otherItemView.getPresenter()); } @@ -232,9 +244,14 @@ void addOwnMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundExce injectionContext.put("dropMessage", dropMessage); injectionContext.put("sender", identity.getAlias()); - NewActionlogItemView myItemView = new NewActionlogItemView(injectionContext::get); + boolean first = lastSender != identity; + lastSender = identity; + ActionlogItemView myItemView = new ActionlogItemView(injectionContext::get); - messages.getChildren().add(myItemView.getView()); + Parent view = myItemView.getView(); + view.getStyleClass().add("received"); + view.getStyleClass().add(first ? "first" : "sequence"); + messages.getChildren().add(view); messageView.add(myItemView); messageControllers.add((ActionlogItem) myItemView.getPresenter()); } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/Hyperlink.java b/src/main/java/de/qabel/desktop/ui/actionlog/Hyperlink.java new file mode 100644 index 00000000..8fdb7fc2 --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/Hyperlink.java @@ -0,0 +1,41 @@ +package de.qabel.desktop.ui.actionlog; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.ActionEvent; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.scene.input.KeyCode; +import javafx.scene.text.Text; + +public class Hyperlink extends Text { + public Hyperlink(String text) { + super(text); + getStyleClass().add("hyperlink"); + + setOnMouseClicked(this::fireActionEvent); + setOnKeyTyped(keyEvent -> { + if (keyEvent.getCode().equals(KeyCode.ENTER)) { + fireActionEvent(keyEvent); + } + }); + } + + protected void fireActionEvent(Event mouseEvent) { + onActionProperty().get().handle(new ActionEvent( + mouseEvent.getSource(), mouseEvent.getTarget() + )); + } + + private ObjectProperty> onAction; + public final ObjectProperty> onActionProperty() { + if (onAction == null) { + onAction = new SimpleObjectProperty<>(this, "onAction", event -> {}); + } + return onAction; + } + + public final void setOnAction(EventHandler handler) { + onActionProperty().setValue(handler); + } +} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/ActionlogItemController.java similarity index 66% rename from src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java rename to src/main/java/de/qabel/desktop/ui/actionlog/item/ActionlogItemController.java index 1b6114f5..7a93b4ba 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemController.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/ActionlogItemController.java @@ -9,18 +9,20 @@ import javafx.fxml.Initializable; import javafx.scene.Node; import javafx.scene.control.Label; -import javafx.scene.text.TextFlow; +import javafx.scene.layout.HBox; import org.jetbrains.annotations.NotNull; -import org.ocpsoft.prettytime.PrettyTime; import javax.inject.Inject; import java.net.URL; +import java.text.SimpleDateFormat; import java.util.ResourceBundle; -public class NewActionlogItemController extends AbstractController implements Initializable, ActionlogItem { +public class ActionlogItemController extends AbstractController implements Initializable, ActionlogItem { + @FXML + Node messageItem; @FXML - TextFlow messageContainer; + HBox messageContainer; @FXML Label dateLabel; @@ -32,25 +34,18 @@ public class NewActionlogItemController extends AbstractController implements In @Inject FXMessageRendererFactory messageRendererFactory; - private PrettyTime p; + private SimpleDateFormat p = new SimpleDateFormat("HH:mm"); @Override public void initialize(URL location, ResourceBundle resources) { - messageContainer.getChildren().clear(); - - p = new PrettyTime(resources.getLocale()); dateLabel.setText(p.format(dropMessage.getCreationDate())); - - Node renderedMessage = getRenderedMessage(sender, resources); - messageContainer.getChildren().addAll(renderedMessage); + messageContainer.getChildren().setAll(getRenderedMessage(sender, resources)); } @NotNull private Node getRenderedMessage(String prefixAlias, ResourceBundle resources) { FXMessageRenderer renderer = messageRendererFactory.getRenderer(dropMessage.getDropPayloadType()); - Node renderedMessage = renderer.render(prefixAlias, dropMessage.getDropPayload(), resources); - renderedMessage.getStyleClass().add("sent"); - return renderedMessage; + return renderer.render(prefixAlias, dropMessage.getDropPayload(), resources); } @Override @@ -62,10 +57,6 @@ public Label getDateLabel() { return dateLabel; } - public void setDateLabel(Label dateLabel) { - this.dateLabel = dateLabel; - } - public DropMessage getDropMessage() { return dropMessage; } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/ActionlogItemView.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/ActionlogItemView.java index d4f9372c..1efbc3bd 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/ActionlogItemView.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/ActionlogItemView.java @@ -1,5 +1,11 @@ package de.qabel.desktop.ui.actionlog.item; +import com.airhacks.afterburner.views.QabelFXMLView; -public interface ActionlogItemView { +import java.util.function.Function; + +public class ActionlogItemView extends QabelFXMLView { + public ActionlogItemView(Function injectionContext) { + super(injectionContext); + } } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java deleted file mode 100644 index 5bca19a7..00000000 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/NewActionlogItemView.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.qabel.desktop.ui.actionlog.item; - -import com.airhacks.afterburner.views.QabelFXMLView; - -import java.util.function.Function; - -public class NewActionlogItemView extends QabelFXMLView implements ActionlogItemView{ - public NewActionlogItemView(Function injectionContext) { - super(injectionContext); - } -} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java index baec1c47..7a583dc9 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRenderer.java @@ -1,65 +1,69 @@ package de.qabel.desktop.ui.actionlog.item.renderer; import de.qabel.desktop.daemon.drop.TextMessage; -import de.qabel.desktop.ui.actionlog.util.QabelChatLabel; +import de.qabel.desktop.ui.actionlog.item.renderer.partial.HyperlinkRenderer; +import de.qabel.desktop.ui.actionlog.item.renderer.partial.PartialFXMessageRenderer; import javafx.scene.Node; -import javafx.scene.control.Label; import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import org.jetbrains.annotations.NotNull; -import java.awt.*; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; import java.util.ResourceBundle; -import java.util.function.Consumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class PlaintextMessageRenderer implements FXMessageRenderer { - private static final String STYLE_CLASS = "message-text"; - public Consumer browserOpener = (uri) -> { - try { - Desktop.getDesktop().browse(new URI(uri)); - } catch (IOException | URISyntaxException ignored) { - } - }; - private static final String DETECT_URI = "(https?:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"; + private final HyperlinkRenderer hyperlinkRenderer = new HyperlinkRenderer(); @Override - public Node render(String prefixAlias, String dropPayload, ResourceBundle resourceBundle) { - String text = renderString(dropPayload, resourceBundle); - return renderTextFlow(prefixAlias, text); + public TextFlow render(String prefixAlias, String dropPayload, ResourceBundle resourceBundle) { + TextFlow flow = new TextFlow(); + flow.getChildren().setAll(renderTextFlowElements(prefixAlias, renderString(dropPayload, resourceBundle))); + return flow; } @NotNull - QabelChatLabel renderTextFlow(String prefixAlias, String message) { - QabelChatLabel node = new QabelChatLabel(prefixAlias, detectUri(message)); - node.getStyleClass().add("text"); - node.getStyleClass().add(STYLE_CLASS); - node.setOnMouseClicked(this::openUriInBrowser); - return node; - } + List renderTextFlowElements(String prefixAlias, String message) { + List children = new LinkedList<>(); + children.add(new Text(message)); + + replaceRenderableChildren(children); - private void openUriInBrowser(javafx.event.Event event) { - Text link = (Text) event.getSource(); - final String uri = link == null ? "" : link.getText(); - new Thread(() -> browserOpener.accept(uri)).start(); - event.consume(); + children.add(0, renderAlias(prefixAlias)); + return children; } - private String detectUri(String message) { - Pattern pattern = Pattern.compile(DETECT_URI, Pattern.CASE_INSENSITIVE | Pattern.COMMENTS | Pattern.MULTILINE); - Matcher matcher = pattern.matcher(message); - return matcher.replaceAll("[$1]"); + private void replaceRenderableChildren(List children) { + List partialRenderers = new LinkedList<>(); + partialRenderers.add(hyperlinkRenderer); + + int size; + do { + size = children.size(); + for (int i = 0; i < children.size(); i++) { + Node child = children.get(i); + if (!(child instanceof Text)) + continue; + + for (PartialFXMessageRenderer renderer : partialRenderers) { + String unrenderedText = ((Text) child).getText(); + if (renderer.needsFormatting(unrenderedText)) { + List replacement = renderer.render(unrenderedText); + children.remove(i); + children.addAll(i, replacement); + i = i + replacement.size() - 1; + break; + } + } + } + } while (size != children.size()); } - @NotNull - @Deprecated - Label renderLabel(String text) { - Label label = new Label(text); - label.getStyleClass().add(STYLE_CLASS); - return label; + private Node renderAlias(String alias) { + Text node = new Text(alias + " "); + node.getStyleClass().add("alias"); + node.managedProperty().bind(node.visibleProperty()); + return node; } @Override @@ -67,5 +71,7 @@ public String renderString(String dropPayload, ResourceBundle resourceBundle) { return TextMessage.fromJson(dropPayload).getText(); } - + public HyperlinkRenderer getHyperlinkRenderer() { + return hyperlinkRenderer; + } } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java index 3886fab6..c73b45d9 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/ShareNotificationRenderer.java @@ -13,7 +13,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; -import java.util.ResourceBundle; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -21,6 +21,7 @@ public class ShareNotificationRenderer implements FXMessageRenderer { private static final ExecutorService renderExecutor = Executors.newSingleThreadExecutor(); private AuthenticatedDownloader downloader; private SharingService sharingService; + private PlaintextMessageRenderer messageRenderer = new PlaintextMessageRenderer(); public ShareNotificationRenderer(AuthenticatedDownloader downloader, SharingService sharingService) { this.downloader = downloader; @@ -28,13 +29,13 @@ public ShareNotificationRenderer(AuthenticatedDownloader downloader, SharingServ } @Override - public Node render(String prefixAlias, String dropPayload, ResourceBundle resourceBundle) { + public Node render(String alias, String dropPayload, ResourceBundle resourceBundle) { VBox result = new VBox(); result.getStyleClass().add("message-text"); result.setStyle("-fx-spacing: 1em;"); ShareNotificationMessage message = ShareNotificationMessage.fromJson(dropPayload); - Label text = new Label(message.getMsg()); + Node text = messageRenderer.render(alias, dropPayload, resourceBundle); result.getChildren().add(text); HBox fileBox = new HBox(); diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java new file mode 100644 index 00000000..9bb8e142 --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java @@ -0,0 +1,39 @@ +package de.qabel.desktop.ui.actionlog.item.renderer.partial; + +import javafx.scene.Node; +import javafx.scene.text.Text; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class AbstractPatternExtractionRenderer implements PartialFXMessageRenderer { + @Override + public boolean needsFormatting(String text) { + return getPattern().matcher(text).find(); + } + + protected abstract Pattern getPattern(); + + @Override + public List render(String text) { + List result = new LinkedList<>(); + Matcher matcher = getPattern().matcher(text); + + int position = 0; + while (matcher.find()) { + if (matcher.start() > position) { + result.add(new Text(text.substring(position, matcher.start()))); + } + result.add(replace(matcher.group(), matcher)); + position = matcher.end(); + } + if (position < text.length() - 1) { + result.add(new Text(text.substring(position))); + } + return result; + } + + protected abstract Node replace(String match, Matcher matcher); +} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java new file mode 100644 index 00000000..5f926058 --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java @@ -0,0 +1,45 @@ +package de.qabel.desktop.ui.actionlog.item.renderer.partial; + +import de.qabel.desktop.ui.actionlog.Hyperlink; +import javafx.event.Event; +import javafx.scene.text.Text; + +import java.awt.*; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HyperlinkRenderer extends AbstractPatternExtractionRenderer { + Consumer browserOpener = (uri) -> new Thread(() -> { + try { + Desktop.getDesktop().browse(new URI(uri)); + } catch (IOException | URISyntaxException ignored) {} + }).start(); + private static final Pattern LINK_PATTERN = Pattern.compile("(https?:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"); + + @Override + protected Pattern getPattern() { + return LINK_PATTERN; + } + + @Override + protected Text replace(String match, Matcher matcher) { + Hyperlink hyperlink = new Hyperlink(match); + hyperlink.setOnAction(this::openUriInBrowser); + return hyperlink; + } + + private void openUriInBrowser(Event event) { + Text link = (Text) event.getTarget(); + final String uri = link == null ? "" : link.getText(); + browserOpener.accept(uri); + event.consume(); + } + + public void setBrowserOpener(Consumer browserOpener) { + this.browserOpener = browserOpener; + } +} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/PartialFXMessageRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/PartialFXMessageRenderer.java new file mode 100644 index 00000000..1efebede --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/PartialFXMessageRenderer.java @@ -0,0 +1,10 @@ +package de.qabel.desktop.ui.actionlog.item.renderer.partial; + +import javafx.scene.Node; + +import java.util.List; + +public interface PartialFXMessageRenderer { + boolean needsFormatting(String text); + List render(String text); +} diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/actionlogitem.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/item/actionlogitem.fxml new file mode 100644 index 00000000..58d12085 --- /dev/null +++ b/src/main/resources/de/qabel/desktop/ui/actionlog/item/actionlogitem.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + sdfh jkhsadkf hkajsdhf kjashd flkajsdhf kjsahdf klashdf kjashdflkjash dfkjsh kasjhd + fkaslhdf kshd fklasjhdf kasljhdf ksajhdf lasdfhaksldhf kasdhf ksajhdf ksahd + fkjsahdfksjahdf lkshdf kasdhfl ksahdf kshdfkasjhdf kasdf + + + + diff --git a/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml b/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml deleted file mode 100644 index ea80fb37..00000000 --- a/src/main/resources/de/qabel/desktop/ui/actionlog/item/newactionlogitem.fxml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - -
- - - - - -
- - - -
diff --git a/src/main/resources/main.css b/src/main/resources/main.css index c1ab0b3d..d0ac9599 100644 --- a/src/main/resources/main.css +++ b/src/main/resources/main.css @@ -20,7 +20,7 @@ * { -fx-font-family: "Source Sans Pro"; - -fx-font-size: 14; + -fx-font-size: 14px; } .text-field.form-control:focused { @@ -270,11 +270,48 @@ -fx-font-weight: bold; } +.message { + -fx-padding: 0 0 0.5em 1em; +} + +.message-text { + -fx-text-fill: #222222; +} .message-text .hyperlink { -fx-fill: #ff690f; -fx-text-fill: #ff690f; } +.message-text .alias { + -fx-font-family: "Source Sans Pro Black", "Source Sans Pro Bold", Bold, Black; + -fx-padding: 0 2em 0 0; +} +.message.sequence .message-text .alias { + visibility: hidden; + -fx-text-fill: transparent; + -fx-max-width: 0; +} + +.sent .message-text .alias { + -fx-fill: orange; +} +.received .message-text .alias { + -fx-fill: darkviolet; +} + +.message-date { + -fx-text-fill: #666666; + -fx-min-width: 2em; + -fx-pref-height: 1em; + -fx-padding: 2px 0 0 0; +} +.message.sequence .message-date { + -fx-text-fill: #CCCCCC; +} +.message-date > .text { + -fx-font-size: 12px!important; + -fx-alignment: bottom-left; +} .payload-type-icon { -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.6), 2, 0.0, 0, 2); @@ -552,7 +589,6 @@ VBox.spaced, HBox.spaced { .button-contextmenu:pressed { -fx-border-color: none; - -fx-text-fill: none; -fx-border-style: none; -fx-text-fill: #ff690f; } diff --git a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java index b178f93c..b3794465 100644 --- a/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java +++ b/src/test-gui/java/de/qabel/desktop/ui/actionlog/ActionlogGuiTest.java @@ -36,7 +36,7 @@ public void typeMessageWithHyperlinkAndClickIt() throws Exception { writeTwoLinesWithOneHyperlink(); submitChat(); - plaintextMessageRenderer.browserOpener = browserOpener::set; + plaintextMessageRenderer.getHyperlinkRenderer().setBrowserOpener(browserOpener::set); fxMessageRendererFactory.setFallbackRenderer(plaintextMessageRenderer); clickOn(".hyperlink"); assertThat(browserOpener.get(), is("http://qabel.de")); diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java index a2ebc4de..d56b5a2a 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java @@ -11,8 +11,8 @@ import de.qabel.desktop.repository.DropMessageRepository; import de.qabel.desktop.repository.inmemory.InMemoryDropMessageRepository; import de.qabel.desktop.ui.AbstractControllerTest; -import de.qabel.desktop.ui.actionlog.item.NewActionlogItemController; -import de.qabel.desktop.ui.actionlog.item.NewActionlogItemView; +import de.qabel.desktop.ui.actionlog.item.ActionlogItemController; +import de.qabel.desktop.ui.actionlog.item.ActionlogItemView; import org.junit.Before; import org.junit.Test; @@ -93,8 +93,8 @@ public void refreshTime() throws Exception { injectionContext.put("dropMessage", d); injectionContext.put("contact", sender); - NewActionlogItemView my = new NewActionlogItemView(injectionContext::get); - NewActionlogItemController messagesController = (NewActionlogItemController) my.getPresenter(); + ActionlogItemView my = new ActionlogItemView(injectionContext::get); + ActionlogItemController messagesController = (ActionlogItemController) my.getPresenter(); controller.messageControllers.add(messagesController); messagesController.setDropMessage(d); diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java index be66a859..8d1baaaf 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/PlaintextMessageRendererTest.java @@ -1,15 +1,17 @@ package de.qabel.desktop.ui.actionlog.item.renderer; +import com.airhacks.afterburner.views.QabelFXMLView; import de.qabel.desktop.ui.AbstractFxTest; -import javafx.scene.control.Labeled; -import org.controlsfx.control.HyperlinkLabel; +import de.qabel.desktop.ui.actionlog.Hyperlink; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import org.junit.Before; import org.junit.Test; -import java.util.concurrent.atomic.AtomicReference; - +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; public class PlaintextMessageRendererTest extends AbstractFxTest { private PlaintextMessageRenderer renderer; @@ -29,24 +31,18 @@ public void rendersPlaintextFromJson() { @Test public void rendersMessageNode() { - String message = renderer.renderString(payload, null); - Labeled node = renderer.renderLabel(message); - assertEquals("content", ((Labeled) node).getText()); + TextFlow node = renderer.render("alias", payload, QabelFXMLView.getDefaultResourceBundle()); + assertEquals("content", ((Text)node.getChildren().get(1)).getText()); } @Test - public void renderHyperlinks() throws Exception { - final String string = "This is a Text,\n" - + " wich has a neat hyperlinks www.qabel.de"; - - String expectedUriFormat = "[www.qabel.de]"; - - AtomicReference browserOpener = new AtomicReference<>(); - renderer.browserOpener = browserOpener::set; - - HyperlinkLabel node = renderer.renderTextFlow("", string); - assertTrue(node.getText().contains(expectedUriFormat)); + public void rendersNodes() { + TextFlow flow = renderer.render("alias", createPayload("contains http://qabel.de"), null); + assertThat(flow.getChildren().size(), equalTo(3)); + assertThat(flow.getChildren().get(2), instanceOf(Hyperlink.class)); } - + private String createPayload(String message) { + return "{'msg': '" + message.replace("'", "\\'") + "'}"; + } } diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java new file mode 100644 index 00000000..2dda85b5 --- /dev/null +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java @@ -0,0 +1,73 @@ +package de.qabel.desktop.ui.actionlog.item.renderer.partial; + +import javafx.scene.Node; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.text.Text; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.Assert.assertTrue; + +public class HyperlinkRendererTest { + private HyperlinkRenderer renderer = new HyperlinkRenderer(); + private AtomicReference lastHyperlink = new AtomicReference<>(); + + @Test + public void splitsAroundHyperlink() { + List nodes = renderer.render("prefix http://qabel.de suffix"); + + assertThat(nodes, hasSize(3)); + assertThat(nodes.get(0), instanceOf(Text.class)); + assertThat(nodes.get(1), instanceOf(Text.class)); + assertThat(nodes.get(2), instanceOf(Text.class)); + + assertThat(((Text) nodes.get(0)).getText(), equalTo("prefix ")); + assertThat(((Text) nodes.get(1)).getText(), equalTo("http://qabel.de")); + assertThat(((Text) nodes.get(2)).getText(), equalTo(" suffix")); + } + + @Test + public void splitsMultipleHyperlinks() { + List nodes = renderer.render("prefix http://qabel.de and http://qabel.org suffix"); + + assertText(nodes.get(0), "prefix "); + assertText(nodes.get(1), "http://qabel.de"); + assertText(nodes.get(2), " and "); + assertText(nodes.get(3), "http://qabel.org"); + assertText(nodes.get(4), " suffix"); + } + + private void assertText(Node node, String text) { + assertThat(node, instanceOf(Text.class)); + assertThat(((Text) node).getText(), equalTo(text)); + } + + @Test + public void createsHyperlinks() { + renderer.browserOpener = lastHyperlink::set; + + Text link = (Text) renderer.render("https://qabel.de").get(0); + assertTrue(link.getStyleClass().contains("hyperlink")); + click(link); + + assertThat(lastHyperlink.get(), equalTo("https://qabel.de")); + } + + protected void click(Text link) { + link.getOnMouseClicked().handle(mouseEvent(link)); + } + + @NotNull + protected MouseEvent mouseEvent(Text link) { + return new MouseEvent(null, link, MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0, MouseButton.PRIMARY, 1, + false, false, false, false, false, false, false, true, false, false, null); + } +} From ef207a06b46ddd3c45c2e707e1e2f1fb10ab1d1c Mon Sep 17 00:00:00 2001 From: julianseeger Date: Wed, 26 Oct 2016 20:08:45 +0200 Subject: [PATCH 12/15] #537 fixups --- .../de/qabel/desktop/DesktopClientGui.java | 2 +- .../ui/actionlog/util/QabelChatLabel.java | 19 ---- .../ui/actionlog/util/QabelChatSkin.java | 93 ------------------- 3 files changed, 1 insertion(+), 113 deletions(-) delete mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java delete mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java diff --git a/src/main/java/de/qabel/desktop/DesktopClientGui.java b/src/main/java/de/qabel/desktop/DesktopClientGui.java index 6a68301d..29b2b831 100644 --- a/src/main/java/de/qabel/desktop/DesktopClientGui.java +++ b/src/main/java/de/qabel/desktop/DesktopClientGui.java @@ -118,7 +118,7 @@ private void showLayoutStage() { Scene layoutScene = new Scene(view, 900, 600, true, SceneAntialiasing.BALANCED); Platform.runLater(() -> primaryStage.setScene(layoutScene)); primaryStage.show(); -// closeStage(); + closeStage(); } private void closeStage() { diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java deleted file mode 100644 index 95194717..00000000 --- a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatLabel.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.qabel.desktop.ui.actionlog.util; - -import javafx.scene.control.Skin; -import org.controlsfx.control.HyperlinkLabel; - -public class QabelChatLabel extends HyperlinkLabel { - - private String prefixAlias; - - public QabelChatLabel(String prefixAlias, String text) { - super(text); - this.prefixAlias = prefixAlias; - } - - @Override - protected Skin createDefaultSkin() { - return new QabelChatSkin(prefixAlias, this); - } -} diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java b/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java deleted file mode 100644 index 73715544..00000000 --- a/src/main/java/de/qabel/desktop/ui/actionlog/util/QabelChatSkin.java +++ /dev/null @@ -1,93 +0,0 @@ -package de.qabel.desktop.ui.actionlog.util; - -import com.sun.javafx.scene.control.behavior.BehaviorBase; -import com.sun.javafx.scene.control.skin.BehaviorSkinBase; -import javafx.scene.Node; -import javafx.scene.control.Control; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -class QabelChatSkin extends BehaviorSkinBase> { - - // The strings used to delimit the hyperlinks - private static final String HYPERLINK_START = "["; //$NON-NLS-1$ - private static final String HYPERLINK_END = "]"; //$NON-NLS-1$ - private TextFlow textFlow; - private final Text senderAlias; - - QabelChatSkin(String senderAlias, QabelChatLabel control) { - super(control, new BehaviorBase<>(control, Collections.emptyList())); - - textFlow = new TextFlow(); - - this.senderAlias = new Text(senderAlias + " "); - this.senderAlias.getStyleClass().add("message-sendername"); - - getChildren().addAll(textFlow); - updateText(); - - registerChangeListener(control.textProperty(), "TEXT"); //$NON-NLS-1$ - } - - @Override - protected void handleControlPropertyChanged(String p) { - super.handleControlPropertyChanged(p); - - if (p.equals("TEXT")) { //$NON-NLS-1$ - updateText(); - } - } - - private void updateText() { - final String text = getSkinnable().getText(); - if (text == null || text.isEmpty()) { - textFlow.getChildren().clear(); - return; - } - // parse the text and put it into an array list - final List nodes = new ArrayList<>(); - - nodes.add(senderAlias); - - int start = 0; - final int textLength = text.length(); - while (start != -1 && start < textLength) { - int startPos = text.indexOf(HYPERLINK_START, start); - int endPos = text.indexOf(HYPERLINK_END, startPos); - - if (isNotHyperlink(startPos, endPos)) { - if (textLength > start) { - // ...but there is still text to turn into one last label - appendTextNode(nodes, text.substring(start)); - break; - } - } - appendTextNode(nodes, text.substring(start, startPos)); - appendHyperlink(nodes, text.substring(startPos + 1, endPos)); - start = endPos + 1; - } - textFlow.setMaxWidth(Control.USE_PREF_SIZE); - textFlow.getChildren().setAll(nodes); - textFlow.requestLayout(); - } - - private boolean isNotHyperlink(int startPos, int endPos) { - return startPos == -1 || endPos == -1; - } - - private void appendTextNode(List nodes, String content) { - Text textnode = new Text(content); - nodes.add(textnode); - } - - private void appendHyperlink(List nodes, String content) { - Text hyperlink = new Text(content); - hyperlink.getStyleClass().add("hyperlink"); - hyperlink.onMouseClickedProperty().bind(getSkinnable().onMouseClickedProperty()); - nodes.add(hyperlink); - } -} From 1d80c595967f89acceb3e430df960e6c3cd7e5d1 Mon Sep 17 00:00:00 2001 From: julianseeger Date: Wed, 26 Oct 2016 20:36:16 +0200 Subject: [PATCH 13/15] #537 fixed message loading --- .../ui/actionlog/ActionlogController.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java index 0d019815..1239e237 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java @@ -136,7 +136,7 @@ private void addListener() { } }); - ((Region) scroller.getContent()).heightProperty().addListener((ov, old_val, new_val) -> { + ((Region) scroller.getContent()).heightProperty().addListener(o -> { if (scroller.getVvalue() != scroller.getVmax()) { scroller.setVvalue(scroller.getVmax()); } @@ -197,7 +197,7 @@ private void addMessagesToView(List dropMessages) { } private void markSeen(PersistenceDropMessage d) { - if (!d.isSeen()) { + if (!d.isSeen() && chat.isVisible()) { d.setSeen(true); System.out.println("marked message seen " + d); executor.submit(() -> { @@ -262,12 +262,22 @@ void setText(String text) { @Override public void update(Observable o, Object arg) { - if (arg instanceof PersistenceDropMessage) { - if (contact == null) { - throw new IllegalStateException("cannot load messages without contact"); - } - executor.submit(() -> tryOrAlert(() -> loadMessages(contact))); + if (!(arg instanceof PersistenceDropMessage)) + return; + PersistenceDropMessage message = (PersistenceDropMessage)arg; + markSeen(message); + + if (contact == null) { + throw new IllegalStateException("cannot load messages without contact"); } + + Platform.runLater(() -> tryOrAlert(() -> { + if (message.getSender() == identity) { + addOwnMessageToActionlog(message.getDropMessage()); + } else { + addMessageToActionlog(message.getDropMessage()); + } + })); } public void setContact(Contact contact) { From ea12491e71301234ae9e797af11c6a897fe42f7b Mon Sep 17 00:00:00 2001 From: julianseeger Date: Thu, 27 Oct 2016 12:51:05 +0200 Subject: [PATCH 14/15] #522 fixups --- CHANGELOG.md | 15 +++ README.md | 6 +- .../ui/actionlog/ActionlogController.java | 90 +++++++------ .../renderer/partial/HyperlinkRenderer.java | 2 +- .../ui/actionlog/ActionlogControllerTest.java | 85 ++++++++---- .../partial/HyperlinkRendererTest.java | 14 +- .../qabel/desktop/util/ImmediateExecutor.java | 123 ++++++++++++++++++ 7 files changed, 265 insertions(+), 70 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/test/java/de/qabel/desktop/util/ImmediateExecutor.java diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..dff2500b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] +### Added +- changelog +- #522 links in chat are now clickable + +### Changed +- #522 new lightweight, TextFlow based chat layout + +## 0.10.0-beta.3 [BASELINE] diff --git a/README.md b/README.md index 03230d06..2ae6ae46 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ This is also used by the script building the windows instal ### Creating the Windows Setup From the `installer` directory you can create a versioned setup with ```BASH -bash build-setup.sh {version} +bash build-setup.sh {version} ``` and {version} needs to be a version of the form `x.y.z`. SemVer compatible prefixes or suffixes are currently not supported because the version is passed directly to launch4j which cannot handle that. Example: @@ -137,7 +137,7 @@ This script will: * create a distribution with `./gradlew -Prelease={version} distZip` * create a wrapping launcher `.exe` for the `jar` * build the setup - + #### creating a dev-release using Vagrant ```BASH vagrant up installer # starts a docker container via vagrant building a setup @@ -219,4 +219,4 @@ For issues using the Qabel Desktop Client, use the **feedback button** ( receivedDropMessages; + private final List receivedDropMessages = new LinkedList<>(); List messageControllers = new LinkedList<>(); Thread dateRefresher; + private Set knownMessages = new HashSet<>(); @Override public void initialize(URL location, ResourceBundle resources) { @@ -95,11 +98,11 @@ public void initialize(URL location, ResourceBundle resources) { dropMessageRepository.addObserver(this); clientConfiguration.onSelectIdentity(identity -> this.identity = identity); addListener(); - contactRepository.attach(this::toggleNotification); + contactRepository.attach(this::updateNotification); scroller.setFitToWidth(true); } - private void toggleNotification() { + private void updateNotification() { notification.setManaged(contact.getStatus() == Contact.ContactStatus.UNKNOWN); } @@ -158,16 +161,13 @@ void sendDropMessage(Contact c, String text) throws QblDropPayloadSizeException, dropMessageRepository.addMessage(d, identity, c, true); } - void loadMessages(Contact c) throws EntityNotFoundException { + private void loadMessages(Contact c) throws EntityNotFoundException { try { - if (receivedDropMessages == null) { + if (receivedDropMessages.isEmpty()) { Platform.runLater(messages.getChildren()::clear); - receivedDropMessages = dropMessageRepository.loadConversation(c, identity); - addMessagesToView(receivedDropMessages); + addMessagesToView(dropMessageRepository.loadConversation(c, identity)); } else { - List newMessages = dropMessageRepository.loadNewMessagesFromConversation(receivedDropMessages, c, identity); - receivedDropMessages.addAll(newMessages); - addMessagesToView(newMessages); + addMessagesToView(dropMessageRepository.loadNewMessagesFromConversation(receivedDropMessages, c, identity)); } } catch (PersistenceException e) { @@ -178,20 +178,11 @@ void loadMessages(Contact c) throws EntityNotFoundException { private void addMessagesToView(List dropMessages) { for (PersistenceDropMessage d : dropMessages) { Platform.runLater(() -> { - if (d.isSent()) { - try { - addOwnMessageToActionlog(d.getDropMessage()); - } catch (EntityNotFoundException e) { - logger.warn("failed to show message: " + e.getMessage(), e); - } - } else { - try { - addMessageToActionlog(d.getDropMessage()); - } catch (EntityNotFoundException e) { - logger.warn("failed to show message: " + e.getMessage(), e); - } + try { + addMessage(d); + } catch (EntityNotFoundException e) { + logger.warn("failed to show message: " + e.getMessage(), e); } - markSeen(d); }); } } @@ -211,7 +202,7 @@ private void markSeen(PersistenceDropMessage d) { } private Entity lastSender; - void addMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundException { + void addReceivedMessage(DropMessage dropMessage) throws EntityNotFoundException { Map injectionContext = new HashMap<>(); String senderKeyId = dropMessage.getSenderKeyId(); if (senderKeyId == null) { @@ -235,7 +226,7 @@ void addMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundExcepti messageControllers.add((ActionlogItem) otherItemView.getPresenter()); } - void addOwnMessageToActionlog(DropMessage dropMessage) throws EntityNotFoundException { + void addSentMessage(DropMessage dropMessage) throws EntityNotFoundException { if (dropMessage.getDropPayload().equals("")) { return; @@ -261,31 +252,50 @@ void setText(String text) { } @Override - public void update(Observable o, Object arg) { + public synchronized void update(Observable o, Object arg) { if (!(arg instanceof PersistenceDropMessage)) return; PersistenceDropMessage message = (PersistenceDropMessage)arg; - markSeen(message); + if (knownMessages.contains(message)) { + return; + } + knownMessages.add(message); + + System.out.println("update: " + message.getDropMessage().getDropPayload() + " on " + Thread.currentThread().getName()); if (contact == null) { throw new IllegalStateException("cannot load messages without contact"); } + if (messageIsFromAnotherConversation(message)) { + return; + } - Platform.runLater(() -> tryOrAlert(() -> { - if (message.getSender() == identity) { - addOwnMessageToActionlog(message.getDropMessage()); - } else { - addMessageToActionlog(message.getDropMessage()); - } - })); + Platform.runLater(() -> tryOrAlert(() -> addMessage(message))); + } + + private boolean messageIsFromAnotherConversation(PersistenceDropMessage message) { + return !(message.isSent() && contact == message.getReceiver() || !message.isSent() && contact == message.getSender()); } - public void setContact(Contact contact) { - receivedDropMessages = null; + protected void addMessage(PersistenceDropMessage message) throws EntityNotFoundException { + knownMessages.add(message); + receivedDropMessages.add(message); + markSeen(message); + if (message.getSender() == identity) { + addSentMessage(message.getDropMessage()); + } else { + addReceivedMessage(message.getDropMessage()); + } + } + + public synchronized void setContact(Contact contact) { + receivedDropMessages.clear(); + knownMessages.clear(); this.contact = contact; - executor.submit(() -> { + lastSender = null; + messageLoadingExecutor.submit(() -> { try { - toggleNotification(); + updateNotification(); loadMessages(this.contact); } catch (Exception e) { alert(e); @@ -294,6 +304,10 @@ public void setContact(Contact contact) { } + void setMessageLoadingExecutor(ExecutorService messageLoadingExecutor) { + this.messageLoadingExecutor = messageLoadingExecutor; + } + public void handleAccept() { saveContact(); } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java index 5f926058..c2446ae7 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java @@ -18,7 +18,7 @@ public class HyperlinkRenderer extends AbstractPatternExtractionRenderer { Desktop.getDesktop().browse(new URI(uri)); } catch (IOException | URISyntaxException ignored) {} }).start(); - private static final Pattern LINK_PATTERN = Pattern.compile("(https?:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"); + private static final Pattern LINK_PATTERN = Pattern.compile("(\\w+:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"); @Override protected Pattern getPattern() { diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java index d56b5a2a..4ceeba8f 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/ActionlogControllerTest.java @@ -13,6 +13,7 @@ import de.qabel.desktop.ui.AbstractControllerTest; import de.qabel.desktop.ui.actionlog.item.ActionlogItemController; import de.qabel.desktop.ui.actionlog.item.ActionlogItemView; +import de.qabel.desktop.util.ImmediateExecutor; import org.junit.Before; import org.junit.Test; @@ -21,28 +22,46 @@ import java.util.Map; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; public class ActionlogControllerTest extends AbstractControllerTest { - ActionlogController controller; - Identity i; - ActionlogView view; - Contact c; - String text = "MessageString"; - DropMessage dm; - InMemoryDropMessageRepository repo; + private ActionlogController controller; + private Identity i; + private ActionlogView view; + private Contact c; + private String text = "MessageString"; + private DropMessage dm; + private InMemoryDropMessageRepository repo; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + repo = new InMemoryDropMessageRepository(); + dropMessageRepository = repo; + super.setUp(); + i = identityBuilderFactory.factory().withAlias("TestAlias").build(); + c = new Contact(i.getAlias(), i.getDropUrls(), i.getEcPublicKey()); + c.setStatus(Contact.ContactStatus.UNKNOWN); + createController(i); + controller.setMessageLoadingExecutor(new ImmediateExecutor()); + controller.setContact(c); + + dm = new DropMessage(c, new TextMessage(text).toJson(), DropMessageRepository.PAYLOAD_TYPE_MESSAGE); + } @Test public void addMessageToActionlogTest() throws Exception { contactRepository.save((Contact) dm.getSender(), i); - controller.addMessageToActionlog(dm); + controller.addReceivedMessage(dm); assertEquals(1, controller.messages.getChildren().size()); } @Test public void addOwnMessageToActionlogTest() throws Exception { - controller.addOwnMessageToActionlog(dm); + controller.addSentMessage(dm); assertEquals(1, controller.messages.getChildren().size()); } @@ -73,6 +92,36 @@ public void switchBetweenIdentitesTest() throws Exception { assertEquals("msg2", TextMessage.fromJson(dropMessage.getDropPayload()).getText()); } + @Test + public void doesNotShowMessagesOfOtherConversations() throws Exception { + clientConfiguration.selectIdentity(i); + contactRepository.save(c, i); + Contact followMe = identityBuilderFactory.factory().withAlias("followMe").build().toContact(); + controller.setContact(followMe); + + controller.sendDropMessage(c, "you won't see this"); + receiveMessageFromC(); + + runLaterAndWait(() -> {}); // wait for the started ui thread to finish + assertEquals(0, controller.messages.getChildren().size()); + } + + @Test + public void showsMessagesOfCurrentConversation() throws Exception { + clientConfiguration.selectIdentity(i); + contactRepository.save(c, i); + + controller.sendDropMessage(c, "you won't see this"); + receiveMessageFromC(); + + runLaterAndWait(() -> {}); // wait for the started ui thread to finish + assertEquals(2, controller.messages.getChildren().size()); + } + + protected void receiveMessageFromC() throws PersistenceException { + dropMessageRepository.addMessage(new DropMessage(c, new TextMessage("won't see this").toJson(), DropMessageRepository.PAYLOAD_TYPE_MESSAGE), c, i, false); + } + @Test public void addDropMessageMetadata() throws Exception { controller.sendDropMessage(c, "msg2"); @@ -153,22 +202,6 @@ public void ignoreContact() throws Exception { }); } - @Override - @Before - public void setUp() throws Exception { - repo = new InMemoryDropMessageRepository(); - dropMessageRepository = repo; - super.setUp(); - i = identityBuilderFactory.factory().withAlias("TestAlias").build(); - c = new Contact(i.getAlias(), i.getDropUrls(), i.getEcPublicKey()); - c.setStatus(Contact.ContactStatus.UNKNOWN); - createController(i); - controller.setContact(c); - controller = (ActionlogController) view.getPresenter(); - - dm = new DropMessage(c, new TextMessage(text).toJson(), DropMessageRepository.PAYLOAD_TYPE_MESSAGE); - } - private void createController(Identity i) { view = new ActionlogView(); clientConfiguration.selectIdentity(i); diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java index 2dda85b5..a7be8990 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java @@ -54,13 +54,23 @@ private void assertText(Node node, String text) { public void createsHyperlinks() { renderer.browserOpener = lastHyperlink::set; - Text link = (Text) renderer.render("https://qabel.de").get(0); - assertTrue(link.getStyleClass().contains("hyperlink")); + Text link = (Text)renderer.render("https://qabel.de").get(0); + assertHyperlink(link); click(link); assertThat(lastHyperlink.get(), equalTo("https://qabel.de")); } + protected void assertHyperlink(Text link) { + assertTrue(link.getStyleClass().contains("hyperlink")); + } + + @Test + public void parsesNonHttpSchemes() { + Text link = (Text)renderer.render("ftp://qabel.de").get(0); + assertHyperlink(link); + } + protected void click(Text link) { link.getOnMouseClicked().handle(mouseEvent(link)); } diff --git a/src/test/java/de/qabel/desktop/util/ImmediateExecutor.java b/src/test/java/de/qabel/desktop/util/ImmediateExecutor.java new file mode 100644 index 00000000..b1b5ff9e --- /dev/null +++ b/src/test/java/de/qabel/desktop/util/ImmediateExecutor.java @@ -0,0 +1,123 @@ +package de.qabel.desktop.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.*; + +public class ImmediateExecutor implements ExecutorService { + private boolean shutdown; + + @Override + public void shutdown() { + shutdown = true; + } + + @NotNull + @Override + public List shutdownNow() { + shutdown(); + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return true; + } + + @NotNull + @Override + public Future submit(Callable task) { + T result = null; + Exception exception = null; + try { + result = task.call(); + } catch (Exception e) { + exception = e; + } + + final T finalResult = result; + final Exception finalException = exception; + return new Future() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + if (finalException == null) { + return finalResult; + } + throw new ExecutionException(finalException); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return get(); + } + }; + } + + @NotNull + @Override + public Future submit(Runnable task, T result) { + return submit(() -> {task.run(); return result;}); + } + + @NotNull + @Override + public Future submit(Runnable task) { + return submit(() -> {task.run(); return true;}); + } + + @NotNull + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + throw new IllegalStateException("not implemented"); + } + + @NotNull + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { + throw new IllegalStateException("not implemented"); + } + + @NotNull + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + throw new IllegalStateException("not implemented"); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + throw new IllegalStateException("not implemented"); + } + + @Override + public void execute(Runnable command) { + command.run(); + } +} From 48d01e126bfa39e4e4c1ad71758b0daddf610727 Mon Sep 17 00:00:00 2001 From: julianseeger Date: Thu, 27 Oct 2016 14:23:45 +0200 Subject: [PATCH 15/15] #522 final url parsing --- .../ui/actionlog/ActionlogController.java | 2 - .../AbstractPatternExtractionRenderer.java | 6 +- .../renderer/partial/HyperlinkRenderer.java | 59 +- .../actionlog/item/renderer/partial/TLD.java | 1531 +++++++++++++++++ .../partial/HyperlinkRendererTest.java | 63 +- 5 files changed, 1649 insertions(+), 12 deletions(-) create mode 100644 src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/TLD.java diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java index 906f1b85..bd00197b 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/ActionlogController.java @@ -190,7 +190,6 @@ private void addMessagesToView(List dropMessages) { private void markSeen(PersistenceDropMessage d) { if (!d.isSeen() && chat.isVisible()) { d.setSeen(true); - System.out.println("marked message seen " + d); executor.submit(() -> { try { dropMessageRepository.save(d); @@ -262,7 +261,6 @@ public synchronized void update(Observable o, Object arg) { } knownMessages.add(message); - System.out.println("update: " + message.getDropMessage().getDropPayload() + " on " + Thread.currentThread().getName()); if (contact == null) { throw new IllegalStateException("cannot load messages without contact"); } diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java index 9bb8e142..8d2689ea 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/AbstractPatternExtractionRenderer.java @@ -11,7 +11,11 @@ public abstract class AbstractPatternExtractionRenderer implements PartialFXMessageRenderer { @Override public boolean needsFormatting(String text) { - return getPattern().matcher(text).find(); + return getDetectionPattern().matcher(text).find(); + } + + protected Pattern getDetectionPattern() { + return getPattern(); } protected abstract Pattern getPattern(); diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java index c2446ae7..f16f2a98 100644 --- a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRenderer.java @@ -3,6 +3,9 @@ import de.qabel.desktop.ui.actionlog.Hyperlink; import javafx.event.Event; import javafx.scene.text.Text; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.awt.*; import java.io.IOException; @@ -13,23 +16,69 @@ import java.util.regex.Pattern; public class HyperlinkRenderer extends AbstractPatternExtractionRenderer { + private static final Logger logger = LoggerFactory.getLogger(HyperlinkRenderer.class); + /** + * scheme + something | scheme + something with a dot + */ + private static final Pattern GENERIC_URL_PATTERN = Pattern.compile("(((\\w+:\\/\\/)|((\\w+:\\/\\/)?[^\\/?#\\s]+\\.))[^?#\\s]+)"); + private static final Pattern IP_PATTERN = Pattern.compile("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"); + Consumer browserOpener = (uri) -> new Thread(() -> { try { Desktop.getDesktop().browse(new URI(uri)); } catch (IOException | URISyntaxException ignored) {} }).start(); - private static final Pattern LINK_PATTERN = Pattern.compile("(\\w+:\\/\\/(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})"); @Override protected Pattern getPattern() { - return LINK_PATTERN; + return GENERIC_URL_PATTERN; } @Override protected Text replace(String match, Matcher matcher) { - Hyperlink hyperlink = new Hyperlink(match); - hyperlink.setOnAction(this::openUriInBrowser); - return hyperlink; + String plaintext = match; + try { + match = checkUri(match); + + Hyperlink hyperlink = new Hyperlink(match); + hyperlink.setOnAction(this::openUriInBrowser); + return hyperlink; + } catch (URISyntaxException ignored) {} + return new Text(plaintext); + } + + @NotNull + private String checkUri(String match) throws URISyntaxException { + URI uri; + try { + uri = new URI(match); + checkScheme(match, uri); + return match; + } catch (URISyntaxException e) { + match = "http://" + match; + uri = new URI(match); + if (hasValidTld(uri) || isIp(uri)) { + return match; + } + } + logger.debug("rejecting " + match + " with host " + uri.getHost()); + throw new URISyntaxException(match, "url is no ip and has no valid tld"); + } + + private boolean isIp(URI uri) { + return IP_PATTERN.matcher(uri.getHost()).matches(); + } + + private void checkScheme(String match, URI uri) throws URISyntaxException { + if (uri.getScheme() == null) { + throw new URISyntaxException(match, "missing scheme"); + } + } + + private boolean hasValidTld(URI uri) throws URISyntaxException { + String host = uri.getHost(); + String tld = host.substring(host.lastIndexOf(".") + 1); + return TLD.isValid(tld); } private void openUriInBrowser(Event event) { diff --git a/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/TLD.java b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/TLD.java new file mode 100644 index 00000000..b82d6415 --- /dev/null +++ b/src/main/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/TLD.java @@ -0,0 +1,1531 @@ +package de.qabel.desktop.ui.actionlog.item.renderer.partial; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class TLD { + public static String[] getAll() { + return ALL; + } + + private static Set set; + public static synchronized Set asSet() { + if (set == null) { + set = new HashSet<>(ALL.length); + Collections.addAll(set, ALL); + } + return set; + } + + public static boolean isValid(String tld) { + return asSet().contains(tld.toUpperCase().intern()); + } + + private static final String[] ALL = { + "AAA", + "AARP", + "ABARTH", + "ABB", + "ABBOTT", + "ABBVIE", + "ABC", + "ABLE", + "ABOGADO", + "ABUDHABI", + "AC", + "ACADEMY", + "ACCENTURE", + "ACCOUNTANT", + "ACCOUNTANTS", + "ACO", + "ACTIVE", + "ACTOR", + "AD", + "ADAC", + "ADS", + "ADULT", + "AE", + "AEG", + "AERO", + "AETNA", + "AF", + "AFAMILYCOMPANY", + "AFL", + "AG", + "AGAKHAN", + "AGENCY", + "AI", + "AIG", + "AIGO", + "AIRBUS", + "AIRFORCE", + "AIRTEL", + "AKDN", + "AL", + "ALFAROMEO", + "ALIBABA", + "ALIPAY", + "ALLFINANZ", + "ALLSTATE", + "ALLY", + "ALSACE", + "ALSTOM", + "AM", + "AMERICANEXPRESS", + "AMERICANFAMILY", + "AMEX", + "AMFAM", + "AMICA", + "AMSTERDAM", + "ANALYTICS", + "ANDROID", + "ANQUAN", + "ANZ", + "AO", + "APARTMENTS", + "APP", + "APPLE", + "AQ", + "AQUARELLE", + "AR", + "ARAMCO", + "ARCHI", + "ARMY", + "ARPA", + "ART", + "ARTE", + "AS", + "ASDA", + "ASIA", + "ASSOCIATES", + "AT", + "ATHLETA", + "ATTORNEY", + "AU", + "AUCTION", + "AUDI", + "AUDIBLE", + "AUDIO", + "AUSPOST", + "AUTHOR", + "AUTO", + "AUTOS", + "AVIANCA", + "AW", + "AWS", + "AX", + "AXA", + "AZ", + "AZURE", + "BA", + "BABY", + "BAIDU", + "BANAMEX", + "BANANAREPUBLIC", + "BAND", + "BANK", + "BAR", + "BARCELONA", + "BARCLAYCARD", + "BARCLAYS", + "BAREFOOT", + "BARGAINS", + "BASKETBALL", + "BAUHAUS", + "BAYERN", + "BB", + "BBC", + "BBT", + "BBVA", + "BCG", + "BCN", + "BD", + "BE", + "BEATS", + "BEAUTY", + "BEER", + "BENTLEY", + "BERLIN", + "BEST", + "BESTBUY", + "BET", + "BF", + "BG", + "BH", + "BHARTI", + "BI", + "BIBLE", + "BID", + "BIKE", + "BING", + "BINGO", + "BIO", + "BIZ", + "BJ", + "BLACK", + "BLACKFRIDAY", + "BLANCO", + "BLOCKBUSTER", + "BLOG", + "BLOOMBERG", + "BLUE", + "BM", + "BMS", + "BMW", + "BN", + "BNL", + "BNPPARIBAS", + "BO", + "BOATS", + "BOEHRINGER", + "BOFA", + "BOM", + "BOND", + "BOO", + "BOOK", + "BOOKING", + "BOOTS", + "BOSCH", + "BOSTIK", + "BOT", + "BOUTIQUE", + "BR", + "BRADESCO", + "BRIDGESTONE", + "BROADWAY", + "BROKER", + "BROTHER", + "BRUSSELS", + "BS", + "BT", + "BUDAPEST", + "BUGATTI", + "BUILD", + "BUILDERS", + "BUSINESS", + "BUY", + "BUZZ", + "BV", + "BW", + "BY", + "BZ", + "BZH", + "CA", + "CAB", + "CAFE", + "CAL", + "CALL", + "CALVINKLEIN", + "CAM", + "CAMERA", + "CAMP", + "CANCERRESEARCH", + "CANON", + "CAPETOWN", + "CAPITAL", + "CAPITALONE", + "CAR", + "CARAVAN", + "CARDS", + "CARE", + "CAREER", + "CAREERS", + "CARS", + "CARTIER", + "CASA", + "CASH", + "CASINO", + "CAT", + "CATERING", + "CBA", + "CBN", + "CBRE", + "CBS", + "CC", + "CD", + "CEB", + "CENTER", + "CEO", + "CERN", + "CF", + "CFA", + "CFD", + "CG", + "CH", + "CHANEL", + "CHANNEL", + "CHASE", + "CHAT", + "CHEAP", + "CHINTAI", + "CHLOE", + "CHRISTMAS", + "CHROME", + "CHRYSLER", + "CHURCH", + "CI", + "CIPRIANI", + "CIRCLE", + "CISCO", + "CITADEL", + "CITI", + "CITIC", + "CITY", + "CITYEATS", + "CK", + "CL", + "CLAIMS", + "CLEANING", + "CLICK", + "CLINIC", + "CLINIQUE", + "CLOTHING", + "CLOUD", + "CLUB", + "CLUBMED", + "CM", + "CN", + "CO", + "COACH", + "CODES", + "COFFEE", + "COLLEGE", + "COLOGNE", + "COM", + "COMCAST", + "COMMBANK", + "COMMUNITY", + "COMPANY", + "COMPARE", + "COMPUTER", + "COMSEC", + "CONDOS", + "CONSTRUCTION", + "CONSULTING", + "CONTACT", + "CONTRACTORS", + "COOKING", + "COOKINGCHANNEL", + "COOL", + "COOP", + "CORSICA", + "COUNTRY", + "COUPON", + "COUPONS", + "COURSES", + "CR", + "CREDIT", + "CREDITCARD", + "CREDITUNION", + "CRICKET", + "CROWN", + "CRS", + "CRUISES", + "CSC", + "CU", + "CUISINELLA", + "CV", + "CW", + "CX", + "CY", + "CYMRU", + "CYOU", + "CZ", + "DABUR", + "DAD", + "DANCE", + "DATE", + "DATING", + "DATSUN", + "DAY", + "DCLK", + "DDS", + "DE", + "DEAL", + "DEALER", + "DEALS", + "DEGREE", + "DELIVERY", + "DELL", + "DELOITTE", + "DELTA", + "DEMOCRAT", + "DENTAL", + "DENTIST", + "DESI", + "DESIGN", + "DEV", + "DHL", + "DIAMONDS", + "DIET", + "DIGITAL", + "DIRECT", + "DIRECTORY", + "DISCOUNT", + "DISCOVER", + "DISH", + "DIY", + "DJ", + "DK", + "DM", + "DNP", + "DO", + "DOCS", + "DOCTOR", + "DODGE", + "DOG", + "DOHA", + "DOMAINS", + "DOT", + "DOWNLOAD", + "DRIVE", + "DTV", + "DUBAI", + "DUCK", + "DUNLOP", + "DUNS", + "DUPONT", + "DURBAN", + "DVAG", + "DVR", + "DZ", + "EARTH", + "EAT", + "EC", + "ECO", + "EDEKA", + "EDU", + "EDUCATION", + "EE", + "EG", + "EMAIL", + "EMERCK", + "ENERGY", + "ENGINEER", + "ENGINEERING", + "ENTERPRISES", + "EPOST", + "EPSON", + "EQUIPMENT", + "ER", + "ERICSSON", + "ERNI", + "ES", + "ESQ", + "ESTATE", + "ESURANCE", + "ET", + "EU", + "EUROVISION", + "EUS", + "EVENTS", + "EVERBANK", + "EXCHANGE", + "EXPERT", + "EXPOSED", + "EXPRESS", + "EXTRASPACE", + "FAGE", + "FAIL", + "FAIRWINDS", + "FAITH", + "FAMILY", + "FAN", + "FANS", + "FARM", + "FARMERS", + "FASHION", + "FAST", + "FEDEX", + "FEEDBACK", + "FERRARI", + "FERRERO", + "FI", + "FIAT", + "FIDELITY", + "FIDO", + "FILM", + "FINAL", + "FINANCE", + "FINANCIAL", + "FIRE", + "FIRESTONE", + "FIRMDALE", + "FISH", + "FISHING", + "FIT", + "FITNESS", + "FJ", + "FK", + "FLICKR", + "FLIGHTS", + "FLIR", + "FLORIST", + "FLOWERS", + "FLY", + "FM", + "FO", + "FOO", + "FOODNETWORK", + "FOOTBALL", + "FORD", + "FOREX", + "FORSALE", + "FORUM", + "FOUNDATION", + "FOX", + "FR", + "FRESENIUS", + "FRL", + "FROGANS", + "FRONTDOOR", + "FRONTIER", + "FTR", + "FUJITSU", + "FUJIXEROX", + "FUND", + "FURNITURE", + "FUTBOL", + "FYI", + "GA", + "GAL", + "GALLERY", + "GALLO", + "GALLUP", + "GAME", + "GAMES", + "GAP", + "GARDEN", + "GB", + "GBIZ", + "GD", + "GDN", + "GE", + "GEA", + "GENT", + "GENTING", + "GEORGE", + "GF", + "GG", + "GGEE", + "GH", + "GI", + "GIFT", + "GIFTS", + "GIVES", + "GIVING", + "GL", + "GLADE", + "GLASS", + "GLE", + "GLOBAL", + "GLOBO", + "GM", + "GMAIL", + "GMBH", + "GMO", + "GMX", + "GN", + "GODADDY", + "GOLD", + "GOLDPOINT", + "GOLF", + "GOO", + "GOODHANDS", + "GOODYEAR", + "GOOG", + "GOOGLE", + "GOP", + "GOT", + "GOV", + "GP", + "GQ", + "GR", + "GRAINGER", + "GRAPHICS", + "GRATIS", + "GREEN", + "GRIPE", + "GROUP", + "GS", + "GT", + "GU", + "GUARDIAN", + "GUCCI", + "GUGE", + "GUIDE", + "GUITARS", + "GURU", + "GW", + "GY", + "HAMBURG", + "HANGOUT", + "HAUS", + "HBO", + "HDFC", + "HDFCBANK", + "HEALTH", + "HEALTHCARE", + "HELP", + "HELSINKI", + "HERE", + "HERMES", + "HGTV", + "HIPHOP", + "HISAMITSU", + "HITACHI", + "HIV", + "HK", + "HKT", + "HM", + "HN", + "HOCKEY", + "HOLDINGS", + "HOLIDAY", + "HOMEDEPOT", + "HOMEGOODS", + "HOMES", + "HOMESENSE", + "HONDA", + "HONEYWELL", + "HORSE", + "HOST", + "HOSTING", + "HOT", + "HOTELES", + "HOTMAIL", + "HOUSE", + "HOW", + "HR", + "HSBC", + "HT", + "HTC", + "HU", + "HUGHES", + "HYATT", + "HYUNDAI", + "IBM", + "ICBC", + "ICE", + "ICU", + "ID", + "IE", + "IEEE", + "IFM", + "IINET", + "IKANO", + "IL", + "IM", + "IMAMAT", + "IMDB", + "IMMO", + "IMMOBILIEN", + "IN", + "INDUSTRIES", + "INFINITI", + "INFO", + "ING", + "INK", + "INSTITUTE", + "INSURANCE", + "INSURE", + "INT", + "INTEL", + "INTERNATIONAL", + "INTUIT", + "INVESTMENTS", + "IO", + "IPIRANGA", + "IQ", + "IR", + "IRISH", + "IS", + "ISELECT", + "ISMAILI", + "IST", + "ISTANBUL", + "IT", + "ITAU", + "ITV", + "IWC", + "JAGUAR", + "JAVA", + "JCB", + "JCP", + "JE", + "JEEP", + "JETZT", + "JEWELRY", + "JLC", + "JLL", + "JM", + "JMP", + "JNJ", + "JO", + "JOBS", + "JOBURG", + "JOT", + "JOY", + "JP", + "JPMORGAN", + "JPRS", + "JUEGOS", + "JUNIPER", + "KAUFEN", + "KDDI", + "KE", + "KERRYHOTELS", + "KERRYLOGISTICS", + "KERRYPROPERTIES", + "KFH", + "KG", + "KH", + "KI", + "KIA", + "KIM", + "KINDER", + "KINDLE", + "KITCHEN", + "KIWI", + "KM", + "KN", + "KOELN", + "KOMATSU", + "KOSHER", + "KP", + "KPMG", + "KPN", + "KR", + "KRD", + "KRED", + "KUOKGROUP", + "KW", + "KY", + "KYOTO", + "KZ", + "LA", + "LACAIXA", + "LADBROKES", + "LAMBORGHINI", + "LAMER", + "LANCASTER", + "LANCIA", + "LANCOME", + "LAND", + "LANDROVER", + "LANXESS", + "LASALLE", + "LAT", + "LATINO", + "LATROBE", + "LAW", + "LAWYER", + "LB", + "LC", + "LDS", + "LEASE", + "LECLERC", + "LEFRAK", + "LEGAL", + "LEGO", + "LEXUS", + "LGBT", + "LI", + "LIAISON", + "LIDL", + "LIFE", + "LIFEINSURANCE", + "LIFESTYLE", + "LIGHTING", + "LIKE", + "LILLY", + "LIMITED", + "LIMO", + "LINCOLN", + "LINDE", + "LINK", + "LIPSY", + "LIVE", + "LIVING", + "LIXIL", + "LK", + "LOAN", + "LOANS", + "LOCKER", + "LOCUS", + "LOFT", + "LOL", + "LONDON", + "LOTTE", + "LOTTO", + "LOVE", + "LPL", + "LPLFINANCIAL", + "LR", + "LS", + "LT", + "LTD", + "LTDA", + "LU", + "LUNDBECK", + "LUPIN", + "LUXE", + "LUXURY", + "LV", + "LY", + "MA", + "MACYS", + "MADRID", + "MAIF", + "MAISON", + "MAKEUP", + "MAN", + "MANAGEMENT", + "MANGO", + "MARKET", + "MARKETING", + "MARKETS", + "MARRIOTT", + "MARSHALLS", + "MASERATI", + "MATTEL", + "MBA", + "MC", + "MCD", + "MCDONALDS", + "MCKINSEY", + "MD", + "ME", + "MED", + "MEDIA", + "MEET", + "MELBOURNE", + "MEME", + "MEMORIAL", + "MEN", + "MENU", + "MEO", + "METLIFE", + "MG", + "MH", + "MIAMI", + "MICROSOFT", + "MIL", + "MINI", + "MINT", + "MIT", + "MITSUBISHI", + "MK", + "ML", + "MLB", + "MLS", + "MM", + "MMA", + "MN", + "MO", + "MOBI", + "MOBILY", + "MODA", + "MOE", + "MOI", + "MOM", + "MONASH", + "MONEY", + "MONSTER", + "MONTBLANC", + "MOPAR", + "MORMON", + "MORTGAGE", + "MOSCOW", + "MOTORCYCLES", + "MOV", + "MOVIE", + "MOVISTAR", + "MP", + "MQ", + "MR", + "MS", + "MSD", + "MT", + "MTN", + "MTPC", + "MTR", + "MU", + "MUSEUM", + "MUTUAL", + "MUTUELLE", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NAB", + "NADEX", + "NAGOYA", + "NAME", + "NATIONWIDE", + "NATURA", + "NAVY", + "NBA", + "NC", + "NE", + "NEC", + "NET", + "NETBANK", + "NETFLIX", + "NETWORK", + "NEUSTAR", + "NEW", + "NEWS", + "NEXT", + "NEXTDIRECT", + "NEXUS", + "NF", + "NFL", + "NG", + "NGO", + "NHK", + "NI", + "NICO", + "NIKE", + "NIKON", + "NINJA", + "NISSAN", + "NISSAY", + "NL", + "NO", + "NOKIA", + "NORTHWESTERNMUTUAL", + "NORTON", + "NOW", + "NOWRUZ", + "NOWTV", + "NP", + "NR", + "NRA", + "NRW", + "NTT", + "NU", + "NYC", + "NZ", + "OBI", + "OBSERVER", + "OFF", + "OFFICE", + "OKINAWA", + "OLAYAN", + "OLAYANGROUP", + "OLDNAVY", + "OLLO", + "OM", + "OMEGA", + "ONE", + "ONG", + "ONL", + "ONLINE", + "ONYOURSIDE", + "OOO", + "OPEN", + "ORACLE", + "ORANGE", + "ORG", + "ORGANIC", + "ORIENTEXPRESS", + "ORIGINS", + "OSAKA", + "OTSUKA", + "OTT", + "OVH", + "PA", + "PAGE", + "PAMPEREDCHEF", + "PANASONIC", + "PANERAI", + "PARIS", + "PARS", + "PARTNERS", + "PARTS", + "PARTY", + "PASSAGENS", + "PAY", + "PCCW", + "PE", + "PET", + "PF", + "PFIZER", + "PG", + "PH", + "PHARMACY", + "PHILIPS", + "PHOTO", + "PHOTOGRAPHY", + "PHOTOS", + "PHYSIO", + "PIAGET", + "PICS", + "PICTET", + "PICTURES", + "PID", + "PIN", + "PING", + "PINK", + "PIONEER", + "PIZZA", + "PK", + "PL", + "PLACE", + "PLAY", + "PLAYSTATION", + "PLUMBING", + "PLUS", + "PM", + "PN", + "PNC", + "POHL", + "POKER", + "POLITIE", + "PORN", + "POST", + "PR", + "PRAMERICA", + "PRAXI", + "PRESS", + "PRIME", + "PRO", + "PROD", + "PRODUCTIONS", + "PROF", + "PROGRESSIVE", + "PROMO", + "PROPERTIES", + "PROPERTY", + "PROTECTION", + "PRU", + "PRUDENTIAL", + "PS", + "PT", + "PUB", + "PW", + "PWC", + "PY", + "QA", + "QPON", + "QUEBEC", + "QUEST", + "QVC", + "RACING", + "RADIO", + "RAID", + "RE", + "READ", + "REALESTATE", + "REALTOR", + "REALTY", + "RECIPES", + "RED", + "REDSTONE", + "REDUMBRELLA", + "REHAB", + "REISE", + "REISEN", + "REIT", + "REN", + "RENT", + "RENTALS", + "REPAIR", + "REPORT", + "REPUBLICAN", + "REST", + "RESTAURANT", + "REVIEW", + "REVIEWS", + "REXROTH", + "RICH", + "RICHARDLI", + "RICOH", + "RIGHTATHOME", + "RIO", + "RIP", + "RO", + "ROCHER", + "ROCKS", + "RODEO", + "ROGERS", + "ROOM", + "RS", + "RSVP", + "RU", + "RUHR", + "RUN", + "RW", + "RWE", + "RYUKYU", + "SA", + "SAARLAND", + "SAFE", + "SAFETY", + "SAKURA", + "SALE", + "SALON", + "SAMSCLUB", + "SAMSUNG", + "SANDVIK", + "SANDVIKCOROMANT", + "SANOFI", + "SAP", + "SAPO", + "SARL", + "SAS", + "SAVE", + "SAXO", + "SB", + "SBI", + "SBS", + "SC", + "SCA", + "SCB", + "SCHAEFFLER", + "SCHMIDT", + "SCHOLARSHIPS", + "SCHOOL", + "SCHULE", + "SCHWARZ", + "SCIENCE", + "SCJOHNSON", + "SCOR", + "SCOT", + "SD", + "SE", + "SEAT", + "SECURE", + "SECURITY", + "SEEK", + "SELECT", + "SENER", + "SERVICES", + "SES", + "SEVEN", + "SEW", + "SEX", + "SEXY", + "SFR", + "SG", + "SH", + "SHANGRILA", + "SHARP", + "SHAW", + "SHELL", + "SHIA", + "SHIKSHA", + "SHOES", + "SHOP", + "SHOPPING", + "SHOUJI", + "SHOW", + "SHOWTIME", + "SHRIRAM", + "SI", + "SILK", + "SINA", + "SINGLES", + "SITE", + "SJ", + "SK", + "SKI", + "SKIN", + "SKY", + "SKYPE", + "SL", + "SLING", + "SM", + "SMART", + "SMILE", + "SN", + "SNCF", + "SO", + "SOCCER", + "SOCIAL", + "SOFTBANK", + "SOFTWARE", + "SOHU", + "SOLAR", + "SOLUTIONS", + "SONG", + "SONY", + "SOY", + "SPACE", + "SPIEGEL", + "SPOT", + "SPREADBETTING", + "SR", + "SRL", + "SRT", + "ST", + "STADA", + "STAPLES", + "STAR", + "STARHUB", + "STATEBANK", + "STATEFARM", + "STATOIL", + "STC", + "STCGROUP", + "STOCKHOLM", + "STORAGE", + "STORE", + "STREAM", + "STUDIO", + "STUDY", + "STYLE", + "SU", + "SUCKS", + "SUPPLIES", + "SUPPLY", + "SUPPORT", + "SURF", + "SURGERY", + "SUZUKI", + "SV", + "SWATCH", + "SWIFTCOVER", + "SWISS", + "SX", + "SY", + "SYDNEY", + "SYMANTEC", + "SYSTEMS", + "SZ", + "TAB", + "TAIPEI", + "TALK", + "TAOBAO", + "TARGET", + "TATAMOTORS", + "TATAR", + "TATTOO", + "TAX", + "TAXI", + "TC", + "TCI", + "TD", + "TDK", + "TEAM", + "TECH", + "TECHNOLOGY", + "TEL", + "TELECITY", + "TELEFONICA", + "TEMASEK", + "TENNIS", + "TEVA", + "TF", + "TG", + "TH", + "THD", + "THEATER", + "THEATRE", + "TIAA", + "TICKETS", + "TIENDA", + "TIFFANY", + "TIPS", + "TIRES", + "TIROL", + "TJ", + "TJMAXX", + "TJX", + "TK", + "TKMAXX", + "TL", + "TM", + "TMALL", + "TN", + "TO", + "TODAY", + "TOKYO", + "TOOLS", + "TOP", + "TORAY", + "TOSHIBA", + "TOTAL", + "TOURS", + "TOWN", + "TOYOTA", + "TOYS", + "TR", + "TRADE", + "TRADING", + "TRAINING", + "TRAVEL", + "TRAVELCHANNEL", + "TRAVELERS", + "TRAVELERSINSURANCE", + "TRUST", + "TRV", + "TT", + "TUBE", + "TUI", + "TUNES", + "TUSHU", + "TV", + "TVS", + "TW", + "TZ", + "UA", + "UBANK", + "UBS", + "UCONNECT", + "UG", + "UK", + "UNICOM", + "UNIVERSITY", + "UNO", + "UOL", + "UPS", + "US", + "UY", + "UZ", + "VA", + "VACATIONS", + "VANA", + "VANGUARD", + "VC", + "VE", + "VEGAS", + "VENTURES", + "VERISIGN", + "VERSICHERUNG", + "VET", + "VG", + "VI", + "VIAJES", + "VIDEO", + "VIG", + "VIKING", + "VILLAS", + "VIN", + "VIP", + "VIRGIN", + "VISA", + "VISION", + "VISTA", + "VISTAPRINT", + "VIVA", + "VIVO", + "VLAANDEREN", + "VN", + "VODKA", + "VOLKSWAGEN", + "VOLVO", + "VOTE", + "VOTING", + "VOTO", + "VOYAGE", + "VU", + "VUELOS", + "WALES", + "WALMART", + "WALTER", + "WANG", + "WANGGOU", + "WARMAN", + "WATCH", + "WATCHES", + "WEATHER", + "WEATHERCHANNEL", + "WEBCAM", + "WEBER", + "WEBSITE", + "WED", + "WEDDING", + "WEIBO", + "WEIR", + "WF", + "WHOSWHO", + "WIEN", + "WIKI", + "WILLIAMHILL", + "WIN", + "WINDOWS", + "WINE", + "WINNERS", + "WME", + "WOLTERSKLUWER", + "WOODSIDE", + "WORK", + "WORKS", + "WORLD", + "WOW", + "WS", + "WTC", + "WTF", + "XBOX", + "XEROX", + "XFINITY", + "XIHUAN", + "XIN", + "XN--11B4C3D", + "XN--1CK2E1B", + "XN--1QQW23A", + "XN--30RR7Y", + "XN--3BST00M", + "XN--3DS443G", + "XN--3E0B707E", + "XN--3OQ18VL8PN36A", + "XN--3PXU8K", + "XN--42C2D9A", + "XN--45BRJ9C", + "XN--45Q11C", + "XN--4GBRIM", + "XN--54B7FTA0CC", + "XN--55QW42G", + "XN--55QX5D", + "XN--5SU34J936BGSG", + "XN--5TZM5G", + "XN--6FRZ82G", + "XN--6QQ986B3XL", + "XN--80ADXHKS", + "XN--80AO21A", + "XN--80ASEHDB", + "XN--80ASWG", + "XN--8Y0A063A", + "XN--90A3AC", + "XN--90AE", + "XN--90AIS", + "XN--9DBQ2A", + "XN--9ET52U", + "XN--9KRT00A", + "XN--B4W605FERD", + "XN--BCK1B9A5DRE4C", + "XN--C1AVG", + "XN--C2BR7G", + "XN--CCK2B3B", + "XN--CG4BKI", + "XN--CLCHC0EA0B2G2A9GCD", + "XN--CZR694B", + "XN--CZRS0T", + "XN--CZRU2D", + "XN--D1ACJ3B", + "XN--D1ALF", + "XN--E1A4C", + "XN--ECKVDTC9D", + "XN--EFVY88H", + "XN--ESTV75G", + "XN--FCT429K", + "XN--FHBEI", + "XN--FIQ228C5HS", + "XN--FIQ64B", + "XN--FIQS8S", + "XN--FIQZ9S", + "XN--FJQ720A", + "XN--FLW351E", + "XN--FPCRJ9C3D", + "XN--FZC2C9E2C", + "XN--FZYS8D69UVGM", + "XN--G2XX48C", + "XN--GCKR3F0F", + "XN--GECRJ9C", + "XN--GK3AT1E", + "XN--H2BRJ9C", + "XN--HXT814E", + "XN--I1B6B1A6A2E", + "XN--IMR513N", + "XN--IO0A7I", + "XN--J1AEF", + "XN--J1AMH", + "XN--J6W193G", + "XN--JLQ61U9W7B", + "XN--JVR189M", + "XN--KCRX77D1X4A", + "XN--KPRW13D", + "XN--KPRY57D", + "XN--KPU716F", + "XN--KPUT3I", + "XN--L1ACC", + "XN--LGBBAT1AD8J", + "XN--MGB9AWBF", + "XN--MGBA3A3EJT", + "XN--MGBA3A4F16A", + "XN--MGBA7C0BBN0A", + "XN--MGBAAM7A8H", + "XN--MGBAB2BD", + "XN--MGBAYH7GPA", + "XN--MGBB9FBPOB", + "XN--MGBBH1A71E", + "XN--MGBC0A9AZCG", + "XN--MGBCA7DZDO", + "XN--MGBERP4A5D4AR", + "XN--MGBPL2FH", + "XN--MGBT3DHD", + "XN--MGBTX2B", + "XN--MGBX4CD0AB", + "XN--MIX891F", + "XN--MK1BU44C", + "XN--MXTQ1M", + "XN--NGBC5AZD", + "XN--NGBE9E0A", + "XN--NODE", + "XN--NQV7F", + "XN--NQV7FS00EMA", + "XN--NYQY26A", + "XN--O3CW4H", + "XN--OGBPF8FL", + "XN--P1ACF", + "XN--P1AI", + "XN--PBT977C", + "XN--PGBS0DH", + "XN--PSSY2U", + "XN--Q9JYB4C", + "XN--QCKA1PMC", + "XN--QXAM", + "XN--RHQV96G", + "XN--ROVU88B", + "XN--S9BRJ9C", + "XN--SES554G", + "XN--T60B56A", + "XN--TCKWE", + "XN--UNUP4Y", + "XN--VERMGENSBERATER-CTB", + "XN--VERMGENSBERATUNG-PWB", + "XN--VHQUV", + "XN--VUQ861B", + "XN--W4R85EL8FHU5DNRA", + "XN--W4RS40L", + "XN--WGBH1C", + "XN--WGBL6A", + "XN--XHQ521B", + "XN--XKC2AL3HYE2A", + "XN--XKC2DL3A5EE0H", + "XN--Y9A3AQ", + "XN--YFRO4I67O", + "XN--YGBI2AMMX", + "XN--ZFR164B", + "XPERIA", + "XXX", + "XYZ", + "YACHTS", + "YAHOO", + "YAMAXUN", + "YANDEX", + "YE", + "YODOBASHI", + "YOGA", + "YOKOHAMA", + "YOU", + "YOUTUBE", + "YT", + "YUN", + "ZA", + "ZAPPOS", + "ZARA", + "ZERO", + "ZIP", + "ZIPPO", + "ZM", + "ZONE", + "ZUERICH", + "ZW" + }; +} diff --git a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java index a7be8990..cea43246 100644 --- a/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java +++ b/src/test/java/de/qabel/desktop/ui/actionlog/item/renderer/partial/HyperlinkRendererTest.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import static junit.framework.TestCase.assertFalse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -61,14 +62,68 @@ public void createsHyperlinks() { assertThat(lastHyperlink.get(), equalTo("https://qabel.de")); } - protected void assertHyperlink(Text link) { - assertTrue(link.getStyleClass().contains("hyperlink")); + @Test + public void parsesNonHttpSchemes() { + Text link = (Text) renderer.render("ftp://qabel.de").get(0); + assertHyperlink(link); } @Test - public void parsesNonHttpSchemes() { - Text link = (Text)renderer.render("ftp://qabel.de").get(0); + public void rendersSpaces() { + List nodes = renderer.render("http://qabel.de http://qabel.de"); + assertThat(nodes, hasSize(3)); + assertThat(((Text)nodes.get(1)).getText(), equalTo(" ")); + } + + @Test + public void detectsUrlWithSubdomainWithoutSchema() { + assertRendersHyperlink("www.qabel.de"); + } + + @Test + public void detectsUrlWithoutSchema() { + assertRendersHyperlink("qabel.de"); + } + + @Test + public void detectsNewTldWithoutSchema() { + assertRendersHyperlink("qabel.computer"); + } + + @Test + public void rejectsSchemalessUrlWithInvalidTLD() { + assertRendersNoHyperlink("qabel.sooooinvalid"); + } + + @Test + public void allowsIps() { + assertRendersHyperlink("http://192.168.1.1:8080/index.html"); + } + + @Test + public void allowsLocalhostWithScheme() { + assertRendersHyperlink("http://localhost"); + } + + private void assertRendersHyperlink(String text) { + assertTrue(renderer.needsFormatting(text)); + List nodes = renderer.render(text); + Text link = (Text) nodes.get(0); assertHyperlink(link); + assertThat(nodes, hasSize(1)); + } + + private void assertRendersNoHyperlink(String text) { + Text link = (Text) renderer.render(text).get(0); + assertNoHyperlink(link); + } + + + protected void assertHyperlink(Text link) { + assertTrue(link.getStyleClass().contains("hyperlink")); + } + protected void assertNoHyperlink(Text link) { + assertFalse(link.getStyleClass().contains("hyperlink")); } protected void click(Text link) {