Skip to content

Commit

Permalink
Initial post step version
Browse files Browse the repository at this point in the history
  • Loading branch information
reinhapa committed Dec 31, 2024
1 parent 8081284 commit 2538776
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2024 TweetWallFX
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.tweetwallfx.conference.stepengine.steps;

import java.util.Arrays;
import java.util.Collection;

import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tweetwallfx.controls.WordleSkin;
import org.tweetwallfx.emoji.control.EmojiFlow;
import org.tweetwallfx.stepengine.api.DataProvider;
import org.tweetwallfx.stepengine.api.Step;
import org.tweetwallfx.stepengine.api.StepEngine.MachineContext;
import org.tweetwallfx.stepengine.api.config.AbstractConfig;
import org.tweetwallfx.stepengine.api.config.StepEngineSettings;
import org.tweetwallfx.stepengine.dataproviders.TweetStreamDataProvider;
import org.tweetwallfx.stepengine.dataproviders.TweetUserProfileImageDataProvider;
import org.tweetwallfx.tweet.api.Tweet;

import javafx.animation.FadeTransition;
import javafx.geometry.Insets;
import javafx.scene.CacheHint;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Duration;

public class ShowPosts implements Step {
private static final Logger LOGGER = LoggerFactory.getLogger(ShowPosts.class);
private final Config config;

private ShowPosts(Config config) {
this.config = config;
}

@Override
public void doStep(MachineContext context) {
final var wordleSkin = (WordleSkin) context.get("WordleSkin");
final var dataProvider = context.getDataProvider(TweetStreamDataProvider.class);

var existingPostsNode = (Pane)wordleSkin.getNode().lookup("#postsNode");
if (null == existingPostsNode) {
LOGGER.debug("Initializing posts node");

var postsNode = new Pane();
postsNode.getStyleClass().add("posts");
postsNode.setId("postsNode");
postsNode.setOpacity(0);

var title = new Label("Latest Posts");

title.setPrefWidth(config.width);
title.getStyleClass().add("title");
title.setPrefHeight(config.titleHeight);
title.setAlignment(Pos.CENTER);

postsNode.getChildren().add(title);

final var fadeIn = new FadeTransition(Duration.millis(500), postsNode);
fadeIn.setFromValue(0);
fadeIn.setToValue(1);
fadeIn.setOnFinished(e -> {
LOGGER.info("Calling proceed from ShowPosts");
context.proceed();
});
postsNode.setLayoutX(config.layoutX);
postsNode.setLayoutY(config.layoutY);
postsNode.setMinWidth(config.width);
postsNode.setMaxWidth(config.width);
postsNode.setPrefWidth(config.width);
postsNode.setCacheHint(CacheHint.SPEED);
postsNode.setCache(true);
int col = 0;
int row = 0;

for (Tweet post : dataProvider.getTweets()) {
Pane postPane = createPostNode(context, post);
double postWidth = (config.width - (config.columns - 1) * config.postHGap) / config.columns;
postPane.setMinWidth(postWidth);
postPane.setMaxWidth(postWidth);
postPane.setPrefWidth(postWidth);
postPane.setMinHeight(config.postHeight);
postPane.setMaxHeight(config.postHeight);
postPane.setPrefHeight(config.postHeight);
postPane.setLayoutX(col * (postWidth + config.postHGap));
postPane.setLayoutY(config.titleHeight + config.postVGap + (config.postHeight + config.postVGap) * row);
postsNode.getChildren().add(postPane);
col++;
if (col == config.columns) {
row++;
col = 0;
}
}

Platform.runLater(() -> {
wordleSkin.getPane().getChildren().add(postsNode);
fadeIn.play();
});
}
}

private Pane createPostNode(MachineContext context, Tweet post) {
final var userImageProvider = context.getDataProvider(TweetUserProfileImageDataProvider.class);
final var bpPostPane = new BorderPane();
bpPostPane.getStyleClass().add("postDisplay");
bpPostPane.setCenter(createSinglePostDisplay(post, userImageProvider));
return bpPostPane;
}

private HBox createSinglePostDisplay(final Tweet post,
final TweetUserProfileImageDataProvider userImageProvider) {
var postFlow = new EmojiFlow();
postFlow.setText(post.getDisplayEnhancedText());
postFlow.setEmojiFitWidth(config.postFontSize);
postFlow.setEmojiFitHeight(config.postFontSize);
postFlow.getStyleClass().add("postFlow");
postFlow.setMaxHeight(Double.MAX_VALUE);

var postUser = new EmojiFlow();
postUser.setText(post.getUser().getName());
postUser.setEmojiFitWidth(config.postUserFontSize);
postUser.setEmojiFitHeight(config.postFontSize);
postUser.getStyleClass().add("postUserName");
postUser.setMaxHeight(Double.MAX_VALUE);

VBox imageBox = new VBox(createPostUserImage(userImageProvider, post));
imageBox.setPadding(new Insets(10, 0, 10, 10));

VBox postContentBox = new VBox(postUser, postFlow);
postContentBox.setSpacing(10);
postContentBox.setPadding(new Insets(10, 10, 10, 0));

HBox postBox = new HBox(imageBox, postContentBox);
postBox.setCacheHint(CacheHint.QUALITY);
postBox.setSpacing(10);

return postBox;
}

private Node createPostUserImage(TweetUserProfileImageDataProvider postImageProvider, Tweet post) {
var image = postImageProvider.getImageBig(post.getUser());
var postUserImage = new ImageView(image);
postUserImage.getStyleClass().add("postUserImage");
postUserImage.setPreserveRatio(true);
if (image.getWidth() > image.getHeight()) {
postUserImage.setFitHeight(config.avatarSize);
} else {
postUserImage.setFitWidth(config.avatarSize);
}

// avatar image clipping
if (config.circularAvatar) {
Circle circle = new Circle(config.avatarSize / 2f, config.avatarSize / 2f, config.avatarSize / 2f);
postUserImage.setClip(circle);
} else {
Rectangle clip = new Rectangle(config.avatarSize, config.avatarSize);
clip.setArcWidth(config.avatarArcSize);
clip.setArcHeight(config.avatarArcSize);
postUserImage.setClip(clip);
}
return postUserImage;
}

static String fixup(String source) {
return source.replaceAll("[\n\r]+", "|").replaceAll("[\u200D]","");
}

@Override
public boolean requiresPlatformThread() {
return false;
}

@Override
public java.time.Duration preferredStepDuration(final MachineContext context) {
return java.time.Duration.ofMillis(config.getStepDuration());
}

/**
* Implementation of {@link Step.Factory} as Service implementation creating
* {@link ShowPosts}.
*/
public static final class FactoryImpl implements Step.Factory {
@Override
public Step create(final StepEngineSettings.StepDefinition stepDefinition) {
return new ShowPosts(stepDefinition.getConfig(ShowPosts.Config.class));
}

@Override
public Class<ShowPosts> getStepClass() {
return ShowPosts.class;
}

@Override
public Collection<Class<? extends DataProvider>> getRequiredDataProviders(final StepEngineSettings.StepDefinition stepSettings) {
return Arrays.asList(TweetStreamDataProvider.class, TweetUserProfileImageDataProvider.class);
}
}

public static class Config extends AbstractConfig {
public double layoutX = 0;
public double layoutY = 0;
public double width = 800;
public double titleHeight = 60;
public double postVGap = 10;
public double postHGap = 10;
public double postHeight = 150;
public int postFontSize = 13;
public int postUserFontSize = 13;
public int columns = 1;
public int avatarSize = 64;
public int avatarArcSize = 20;
public boolean circularAvatar = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,8 @@ public void doStep(final MachineContext context) {
int col = 0;
int row = 0;

Iterator<SessionData> iterator = dataProvider.getFilteredSessionData().iterator();
String oldRoom = null;
while (iterator.hasNext()) {
var sessionData = iterator.next();
for (SessionData sessionData : dataProvider.getFilteredSessionData()) {
if (null == oldRoom) {
oldRoom = sessionData.room.getName();
}
Expand Down Expand Up @@ -266,7 +264,7 @@ private Pane createSessionNode(final MachineContext context, final SessionData s
if (config.showTags) {
var tags = new FlowPane();
tags.getStyleClass().add("tags");
sessionData.tags.stream().forEach(tag -> {
sessionData.tags.forEach(tag -> {
var tagLabel = new Label(tag);
tagLabel.getStyleClass().add("tagLabel");
tags.getChildren().add(tagLabel);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.tweetwallfx.conference.stepengine.steps.ShowPosts$FactoryImpl
org.tweetwallfx.conference.stepengine.steps.ShowSchedule$FactoryImpl
org.tweetwallfx.conference.stepengine.steps.ShowTopRated$FactoryImpl
org.tweetwallfx.conference.stepengine.steps.SpeakerImageMosaicStep$FactoryImpl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015-2023 TweetWallFX
* Copyright (c) 2015-2024 TweetWallFX
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand All @@ -24,8 +24,14 @@
package org.tweetwallfx.tweet.impl.mastodon4j;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.safety.Cleaner;
import org.jsoup.safety.Safelist;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import org.mastodon4j.core.api.entities.Status;
import org.tweetwallfx.tweet.api.Tweet;
import org.tweetwallfx.tweet.api.User;
Expand All @@ -36,14 +42,42 @@
import org.tweetwallfx.tweet.api.entry.UserMentionTweetEntry;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.StringJoiner;

final class MastodonStatus implements Tweet {

private final Status status;
private final String text;
private final List<HashtagTweetEntry> hashtagTweetEntryList;

public MastodonStatus(Status status) {
this.status = status;
Cleaner cleaner = new Cleaner(Safelist.none().addTags("img", "movie", "a"));
Document document = cleaner.clean(Jsoup.parse(status.content()));
hashtagTweetEntryList = new ArrayList<>();
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(Node node, int i) {
if (node instanceof TextNode textNode) {
//String wholeText = textNode.getWholeText();
hashtagTweetEntryList.add(null);
} else if (node instanceof Element element) {
switch (element.nodeName()) {
case "a" -> {
}
case "img" -> {
}
case "movie" -> {
}
default -> {
}
}
}
}
}, document);
this.text = document.text();
}

@Override
Expand Down Expand Up @@ -106,8 +140,7 @@ public Tweet getOriginTweet() {

@Override
public String getText() {
Cleaner cleaner = new Cleaner(Safelist.none());
return cleaner.clean(Jsoup.parse(status.content())).text();
return text;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 TweetWallFX
* Copyright (c) 2023-2024 TweetWallFX
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -32,9 +32,13 @@
import static org.assertj.core.api.Assertions.assertThat;

class MastodonStatusTest {
public static final String STATUS_CONTENT = """
<p>the status #message html to @TweetwallFX, a new line:
some nice image <img src="https://somehost.somedomain/image.jpg alt="some image">
and a <a href="http://somehost.somedomain/">#special link</a> to some content for <b>@reinhapa</b></p>""";
ZonedDateTime createdAt = ZonedDateTime.now(ZoneId.systemDefault());
MastodonStatus status = new MastodonStatus(new Status("42", null, createdAt, null,
"<p>the status message html</p>", null, null, null, null,
STATUS_CONTENT, null, null, null, null,
null, null, null, null, 33, 22, null,
null, null, null, null, null, null, "german",
null, null, null, true, null, null, null, null));
Expand Down Expand Up @@ -107,7 +111,9 @@ void getOriginTweet() {

@Test
void getText() {
assertThat(status.getText()).isEqualTo("the status message html");
assertThat(status.getText()).isEqualTo("""
the status #message html to @TweetwallFX, a new line: \
some nice image and a #special link to some content for @reinhapa""");
assertThat(statusWithoutOptionals.getText()).isEmpty();
}

Expand Down

0 comments on commit 2538776

Please sign in to comment.