diff --git a/build.gradle b/build.gradle
index 1fd7d5fbf2..c3a67ff284 100644
--- a/build.gradle
+++ b/build.gradle
@@ -357,7 +357,6 @@ dependencies {
testImplementation("org.testfx:testfx-junit5:4.0.17")
testImplementation("com.natpryce.hamcrest:hamcrest-reflection:0.1-2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
- testImplementation("org.testfx:openjfx-monocle:jdk-12.0.1+2")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
annotationProcessor("org.projectlombok:lombok")
diff --git a/src/main/java/com/faforever/client/chat/AbstractChatTabController.java b/src/main/java/com/faforever/client/chat/AbstractChatTabController.java
index 8fb8a9e261..804855bc2b 100644
--- a/src/main/java/com/faforever/client/chat/AbstractChatTabController.java
+++ b/src/main/java/com/faforever/client/chat/AbstractChatTabController.java
@@ -7,16 +7,11 @@
import com.faforever.client.exception.AssetLoadException;
import com.faforever.client.fx.FxApplicationThreadExecutor;
import com.faforever.client.fx.JavaFxUtil;
-import com.faforever.client.fx.SimpleChangeListener;
-import com.faforever.client.fx.SimpleInvalidationListener;
import com.faforever.client.fx.TabController;
import com.faforever.client.fx.WebViewConfigurer;
import com.faforever.client.i18n.I18n;
-import com.faforever.client.main.event.NavigateEvent;
-import com.faforever.client.main.event.NavigationItem;
import com.faforever.client.navigation.NavigationHandler;
import com.faforever.client.notification.NotificationService;
-import com.faforever.client.notification.TransientNotification;
import com.faforever.client.player.CountryFlagService;
import com.faforever.client.player.PlayerService;
import com.faforever.client.preferences.ChatPrefs;
@@ -25,22 +20,16 @@
import com.faforever.client.ui.StageHolder;
import com.faforever.client.user.LoginService;
import com.faforever.client.util.ConcurrentUtil;
-import com.faforever.client.util.IdenticonUtil;
import com.faforever.client.util.PopupUtil;
import com.faforever.client.util.TimeService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.io.CharStreams;
-import javafx.beans.InvalidationListener;
-import javafx.beans.Observable;
-import javafx.beans.WeakInvalidationListener;
-import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
-import javafx.beans.value.WeakChangeListener;
import javafx.concurrent.Worker.State;
import javafx.css.PseudoClass;
import javafx.event.Event;
@@ -57,7 +46,7 @@
import javafx.stage.Popup;
import javafx.stage.PopupWindow;
import javafx.stage.PopupWindow.AnchorLocation;
-import javafx.stage.Stage;
+import javafx.stage.Window;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import netscape.javascript.JSObject;
@@ -102,10 +91,14 @@ public abstract class AbstractChatTabController extends TabController {
private static final String MESSAGE_CONTAINER_ID = "chat-container";
private static final String MESSAGE_ITEM_CLASS = "chat-section";
private static final PseudoClass UNREAD_PSEUDO_STATE = PseudoClass.getPseudoClass("unread");
- private static final org.springframework.core.io.Resource CHAT_JS_RESOURCE = new ClassPathResource("/js/chat_container.js");
- private static final org.springframework.core.io.Resource AUTOLINKER_JS_RESOURCE = new ClassPathResource("/js/Autolinker.min.js");
- private static final org.springframework.core.io.Resource JQUERY_JS_RESOURCE = new ClassPathResource("js/jquery-2.1.4.min.js");
- private static final org.springframework.core.io.Resource JQUERY_HIGHLIGHT_JS_RESOURCE = new ClassPathResource("js/jquery.highlight-5.closure.js");
+ private static final org.springframework.core.io.Resource CHAT_JS_RESOURCE = new ClassPathResource(
+ "/js/chat_container.js");
+ private static final org.springframework.core.io.Resource AUTOLINKER_JS_RESOURCE = new ClassPathResource(
+ "/js/Autolinker.min.js");
+ private static final org.springframework.core.io.Resource JQUERY_JS_RESOURCE = new ClassPathResource(
+ "js/jquery-2.1.4.min.js");
+ private static final org.springframework.core.io.Resource JQUERY_HIGHLIGHT_JS_RESOURCE = new ClassPathResource(
+ "js/jquery.highlight-5.closure.js");
private static final String CHANNEL_LINK_HTML_FORMAT = "%1$s";
/**
@@ -148,10 +141,6 @@ public abstract class AbstractChatTabController extends TabController {
protected final ObjectProperty chatChannel = new SimpleObjectProperty<>();
protected final ObservableValue channelName = chatChannel.map(ChatChannel::getName);
- private final SimpleInvalidationListener stageFocusedListener = this::focusTextFieldIfStageFocused;
- private final SimpleInvalidationListener resetUnreadMessagesListener = this::clearUnreadIfFocused;
- private final SimpleChangeListener tabPaneFocusedListener = this::onTabPaneFocused;
-
private final Consumer messageListener = this::onChatMessage;
private int lastEntryId;
@@ -169,45 +158,49 @@ public abstract class AbstractChatTabController extends TabController {
@Override
protected void onInitialize() {
- BooleanExpression tabPaneShowing = BooleanExpression.booleanExpression(getRoot().tabPaneProperty()
- .flatMap(
- uiService::createShowingProperty));
- ObservableValue showing = getRoot().selectedProperty()
- .and(tabPaneShowing);
-
- mentionPattern = Pattern.compile("(^|[^A-Za-z0-9-])" + Pattern.quote(loginService.getUsername()) + "([^A-Za-z0-9-]|$)", CASE_INSENSITIVE);
+ mentionPattern = Pattern.compile(
+ "(^|[^A-Za-z0-9-])" + Pattern.quote(loginService.getUsername()) + "([^A-Za-z0-9-]|$)", CASE_INSENSITIVE);
initChatView();
- addFocusListeners();
-
- chatChannel.when(showing).subscribe(((oldValue, newValue) -> {
+ chatChannel.when(attached).subscribe(((oldValue, newValue) -> {
if (oldValue != null) {
+ oldValue.openProperty().unbind();
oldValue.removeMessageListener(messageListener);
}
if (newValue != null) {
+ newValue.openProperty().bind(showing.and(StageHolder.getStage().focusedProperty()).when(attached));
newValue.addMessageListener(messageListener);
}
}));
- showing.subscribe(shown -> {
- ChatChannel channel = chatChannel.get();
- if (!shown && channel != null) {
- channel.removeMessageListener(messageListener);
+ showing.subscribe(selected -> {
+ clearUnreadIfFocused();
+ if (selected) {
+ // We have to request focus after the tab is fully added to the scene which happens sometime after selection
+ fxApplicationThreadExecutor.runLater(messageTextField()::requestFocus);
}
});
- unreadMessagesCount.addListener((observable, oldValue, newValue) -> incrementUnreadMessageCount(newValue.intValue() - oldValue.intValue()));
- StageHolder.getStage().focusedProperty().addListener(new WeakInvalidationListener(resetUnreadMessagesListener));
- getRoot().selectedProperty().addListener(new WeakInvalidationListener(resetUnreadMessagesListener));
-
+ unreadMessagesCount.subscribe(
+ (oldValue, newValue) -> incrementUnreadMessageCount(newValue.intValue() - oldValue.intValue()));
getRoot().setOnClosed(this::onClosed);
}
- private void focusTextFieldIfStageFocused() {
- Tab root = getRoot();
- if (root != null && root.getTabPane() != null && root.getTabPane().isVisible()) {
- fxApplicationThreadExecutor.execute(() -> messageTextField().requestFocus());
+ @Override
+ public void onAttached() {
+ addAttachedSubscription(StageHolder.getStage().focusedProperty().subscribe(() -> {
+ clearUnreadIfFocused();
+ messageTextField().requestFocus();
+ }));
+ }
+
+ @Override
+ public void onDetached() {
+ ChatChannel channel = chatChannel.get();
+ if (channel != null) {
+ channel.openProperty().unbind();
+ channel.removeMessageListener(messageListener);
}
}
@@ -217,14 +210,6 @@ private void clearUnreadIfFocused() {
}
}
- private void addTabListeners(TabPane newTabPane) {
- if (newTabPane == null) {
- return;
- }
- StageHolder.getStage().focusedProperty().addListener(new WeakInvalidationListener(stageFocusedListener));
- newTabPane.focusedProperty().addListener(new WeakChangeListener<>(tabPaneFocusedListener));
- }
-
/**
* Returns true if this chat tab is currently focused by the user. Returns false if a different tab is selected, the
* user is not in "chat" or if the window has no focus.
@@ -235,9 +220,16 @@ protected boolean hasFocus() {
}
TabPane tabPane = getRoot().getTabPane();
- return tabPane != null && JavaFxUtil.isVisibleRecursively(tabPane) && tabPane.getScene()
- .getWindow()
- .isFocused() && tabPane.getScene().getWindow().isShowing();
+ if (tabPane == null) {
+ return false;
+ }
+
+ if (!JavaFxUtil.isVisibleRecursively(tabPane)) {
+ return false;
+ }
+
+ Window window = tabPane.getScene().getWindow();
+ return window.isFocused() && window.isShowing();
}
protected void setUnread(boolean unread) {
@@ -254,8 +246,8 @@ protected void setUnread(boolean unread) {
// Tab has been closed
return;
}
- Node tab = (Node) skin.queryAccessibleAttribute(ITEM_AT_INDEX, tabIndex);
- tab.pseudoClassStateChanged(UNREAD_PSEUDO_STATE, unread);
+ Node tabSkin = (Node) skin.queryAccessibleAttribute(ITEM_AT_INDEX, tabIndex);
+ tabSkin.pseudoClassStateChanged(UNREAD_PSEUDO_STATE, unread);
if (!unread) {
synchronized (unreadMessagesCount) {
@@ -298,21 +290,6 @@ protected void onClosed(Event event) {
messageTextField().setOnKeyReleased(null);
}
- /**
- * Registers listeners necessary to focus the message input field when changing to another message tab, changing from
- * another tab to the "chat" tab or re-focusing the window.
- */
- private void addFocusListeners() {
- getRoot().selectedProperty().addListener((observable, oldValue, newValue) -> {
- if (newValue) {
- // Since a tab is marked as "selected" before it's rendered, the text field can't be selected yet.
- // So let's schedule the focus to be executed afterwards
- fxApplicationThreadExecutor.execute(messageTextField()::requestFocus);
- }
- });
- getRoot().tabPaneProperty().addListener((tabPane, oldTabPane, newTabPane) -> addTabListeners(newTabPane));
- }
-
protected abstract TextInputControl messageTextField();
private void initChatView() {
@@ -328,10 +305,12 @@ private void initChatView() {
private void loadChatContainer() {
try (Reader reader = new InputStreamReader(uiService.getThemeFileUrl(CHAT_CONTAINER).openStream())) {
String chatContainerHtml = CharStreams.toString(reader)
- .replace("{chat-container-js}", CHAT_JS_RESOURCE.getURL().toExternalForm())
- .replace("{auto-linker-js}", AUTOLINKER_JS_RESOURCE.getURL().toExternalForm())
- .replace("{jquery-js}", JQUERY_JS_RESOURCE.getURL().toExternalForm())
- .replace("{jquery-highlight-js}", JQUERY_HIGHLIGHT_JS_RESOURCE.getURL().toExternalForm());
+ .replace("{chat-container-js}", CHAT_JS_RESOURCE.getURL().toExternalForm())
+ .replace("{auto-linker-js}",
+ AUTOLINKER_JS_RESOURCE.getURL().toExternalForm())
+ .replace("{jquery-js}", JQUERY_JS_RESOURCE.getURL().toExternalForm())
+ .replace("{jquery-highlight-js}",
+ JQUERY_HIGHLIGHT_JS_RESOURCE.getURL().toExternalForm());
engine.loadContent(chatContainerHtml);
} catch (IOException e) {
@@ -347,8 +326,8 @@ private void configureBrowser(WebView messagesWebView) {
private void configureLoadListener() {
engine.getLoadWorker()
- .stateProperty()
- .addListener((observable, oldValue, newValue) -> sendWaitingMessagesIfLoaded(newValue));
+ .stateProperty()
+ .addListener((observable, oldValue, newValue) -> sendWaitingMessagesIfLoaded(newValue));
}
private void sendWaitingMessagesIfLoaded(State newValue) {
@@ -360,7 +339,6 @@ private void sendWaitingMessagesIfLoaded(State newValue) {
waitingMessages.forEach(AbstractChatTabController.this::addMessage);
waitingMessages.clear();
isChatReady = true;
- onWebViewLoaded();
}
}
@@ -378,16 +356,6 @@ protected void callJsMethod(String methodName, Object... args) {
}
}
- private void onTabPaneFocused(Boolean newTabPaneFocus) {
- if (newTabPaneFocus) {
- fxApplicationThreadExecutor.execute(() -> messageTextField().requestFocus());
- }
- }
-
- protected void onWebViewLoaded() {
- // Default implementation does nothing, can be overridden by subclass.
- }
-
public void onSendMessage() {
TextInputControl messageTextField = messageTextField();
@@ -445,18 +413,18 @@ private void sendAction(final TextInputControl messageTextField, final String te
messageTextField.setDisable(true);
chatService.sendActionInBackground(chatChannel.get(), text.replaceFirst(Pattern.quote(ACTION_PREFIX), ""))
- .thenRunAsync(() -> {
- messageTextField.clear();
- messageTextField.setDisable(false);
- messageTextField.requestFocus();
- }, fxApplicationThreadExecutor)
- .exceptionally(throwable -> {
- throwable = ConcurrentUtil.unwrapIfCompletionException(throwable);
- log.warn("Message could not be sent: {}", text, throwable);
- notificationService.addImmediateErrorNotification(throwable, "chat.sendFailed");
- fxApplicationThreadExecutor.execute(() -> messageTextField.setDisable(false));
- return null;
- });
+ .thenRunAsync(() -> {
+ messageTextField.clear();
+ messageTextField.setDisable(false);
+ messageTextField.requestFocus();
+ }, fxApplicationThreadExecutor)
+ .exceptionally(throwable -> {
+ throwable = ConcurrentUtil.unwrapIfCompletionException(throwable);
+ log.warn("Message could not be sent: {}", text, throwable);
+ notificationService.addImmediateErrorNotification(throwable, "chat.sendFailed");
+ fxApplicationThreadExecutor.execute(() -> messageTextField.setDisable(false));
+ return null;
+ });
}
protected void onChatMessage(ChatMessage chatMessage) {
@@ -482,7 +450,8 @@ private void removeTopmostMessages() {
JavaFxUtil.assertApplicationThread();
int maxMessageItems = chatPrefs.getMaxMessages();
- int numberOfMessages = (int) engine.executeScript("document.getElementsByClassName('" + MESSAGE_ITEM_CLASS + "').length");
+ int numberOfMessages = (int) engine.executeScript(
+ "document.getElementsByClassName('" + MESSAGE_ITEM_CLASS + "').length");
while (numberOfMessages > maxMessageItems) {
engine.executeScript("document.getElementsByClassName('" + MESSAGE_ITEM_CLASS + "')[0].remove()");
numberOfMessages--;
@@ -508,8 +477,16 @@ private void addMessage(ChatMessage chatMessage) {
}
private boolean requiresNewChatSection(ChatMessage chatMessage) {
- return lastMessage == null || !lastMessage.username().equals(chatMessage.username()) || lastMessage.time()
- .isBefore(chatMessage.time().minus(1, MINUTES)) || lastMessage.action();
+ if (lastMessage == null) {
+ return true;
+ }
+ if (!lastMessage.username().equals(chatMessage.username())) {
+ return true;
+ }
+ if (lastMessage.time().isBefore(chatMessage.time().minus(1, MINUTES))) {
+ return true;
+ }
+ return lastMessage.action();
}
private void appendMessage(ChatMessage chatMessage) throws IOException {
@@ -551,20 +528,20 @@ private String renderHtml(ChatMessage chatMessage, URL themeFileUrl, @Nullable I
String avatarUrl = playerOptional.map(PlayerBean::getAvatar).map(AvatarBean::getUrl).map(URL::toString).orElse("");
String countryFlagUrl = playerOptional.map(PlayerBean::getCountry)
- .flatMap(countryFlagService::getCountryFlagUrl)
- .map(URL::toString)
- .orElse("");
+ .flatMap(countryFlagService::getCountryFlagUrl)
+ .map(URL::toString)
+ .orElse("");
String clanTag = clanOptional.orElse("");
String decoratedClanTag = clanOptional.map(tag -> i18n.get("chat.clanTagFormat", tag)).orElse("");
String timeString = timeService.asShortTime(chatMessage.time());
html = html.replace("{time}", timeString)
- .replace("{avatar}", avatarUrl)
- .replace("{username}", username)
- .replace("{clan-tag}", clanTag)
- .replace("{decorated-clan-tag}", decoratedClanTag)
- .replace("{country-flag}", StringUtils.defaultString(countryFlagUrl))
- .replace("{section-id}", String.valueOf(sectionId));
+ .replace("{avatar}", avatarUrl)
+ .replace("{username}", username)
+ .replace("{clan-tag}", clanTag)
+ .replace("{decorated-clan-tag}", decoratedClanTag)
+ .replace("{country-flag}", StringUtils.defaultString(countryFlagUrl))
+ .replace("{section-id}", String.valueOf(sectionId));
Collection cssClasses = new ArrayList<>();
cssClasses.add(String.format("user-%s", chatMessage.username()));
@@ -590,16 +567,18 @@ private String renderHtml(ChatMessage chatMessage, URL themeFileUrl, @Nullable I
}
return html.replace("{css-classes}", Joiner.on(' ').join(cssClasses))
- .replace("{inline-style}", getInlineStyle(username))
- // Always replace text last in case the message contains one of the placeholders.
- .replace("{text}", text);
+ .replace("{inline-style}", getInlineStyle(username))
+ // Always replace text last in case the message contains one of the placeholders.
+ .replace("{text}", text);
}
@VisibleForTesting
protected String transformEmoticonShortcodesToImages(String text) {
return emoticonService.getEmoticonShortcodeDetectorPattern()
- .matcher(text)
- .replaceAll((matchResult) -> String.format(EMOTICON_IMG_TEMPLATE, emoticonService.getBase64SvgContentByShortcode(matchResult.group())));
+ .matcher(text)
+ .replaceAll((matchResult) -> String.format(EMOTICON_IMG_TEMPLATE,
+ emoticonService.getBase64SvgContentByShortcode(
+ matchResult.group())));
}
@VisibleForTesting
@@ -619,25 +598,6 @@ protected void onMention(ChatMessage chatMessage) {
// Default implementation does nothing
}
- protected void showNotificationIfNecessary(ChatMessage chatMessage) {
- Stage stage = StageHolder.getStage();
- if (stage.isFocused() && stage.isShowing()) {
- return;
- }
-
- Optional playerOptional = playerService.getPlayerByNameIfOnline(chatMessage.username());
- String identIconSource = playerOptional.map(player -> String.valueOf(player.getId()))
- .orElseGet(chatMessage::username);
-
- if (notificationPrefs.isPrivateMessageToastEnabled()) {
- notificationService.addNotification(new TransientNotification(chatMessage.username(), chatMessage.message(), IdenticonUtil.createIdenticon(identIconSource), event -> {
- navigationHandler.navigateTo(new NavigateEvent(NavigationItem.CHAT));
- stage.toFront();
- getRoot().getTabPane().getSelectionModel().select(getRoot());
- }));
- }
- }
-
protected String getMessageCssClass(String login) {
Optional playerOptional = playerService.getPlayerByNameIfOnline(login);
if (playerOptional.isEmpty()) {
@@ -663,7 +623,8 @@ protected String convertUrlsToHyperlinks(String text) {
}
private void insertIntoContainer(String html, String containerId) {
- ((JSObject) engine.executeScript("document.getElementById('" + containerId + "')")).call("insertAdjacentHTML", "beforeend", html);
+ ((JSObject) engine.executeScript("document.getElementById('" + containerId + "')")).call("insertAdjacentHTML",
+ "beforeend", html);
getMessagesWebView().requestLayout();
}
@@ -671,19 +632,6 @@ private void insertIntoContainer(String html, String containerId) {
* Subclasses may override in order to perform actions when the view is being displayed.
*/
protected void onDisplay() {
- // If channel tab has just been created, scene for the text field does not initialize immediately
- if (messageTextField().getScene() == null) {
- InvalidationListener listener = new InvalidationListener() {
- @Override
- public void invalidated(Observable observable) {
- messageTextField().sceneProperty().removeListener(this);
- fxApplicationThreadExecutor.execute(() -> messageTextField().requestFocus());
- }
- };
- JavaFxUtil.addListener(messageTextField().sceneProperty(), listener);
- } else {
- fxApplicationThreadExecutor.execute(() -> messageTextField().requestFocus());
- }
}
public void openEmoticonsPopupWindow() {
diff --git a/src/main/java/com/faforever/client/chat/ChannelTabController.java b/src/main/java/com/faforever/client/chat/ChannelTabController.java
index 8321647cb7..354b2b8d5d 100644
--- a/src/main/java/com/faforever/client/chat/ChannelTabController.java
+++ b/src/main/java/com/faforever/client/chat/ChannelTabController.java
@@ -1,13 +1,10 @@
package com.faforever.client.chat;
-import com.faforever.client.audio.AudioService;
import com.faforever.client.chat.emoticons.EmoticonService;
import com.faforever.client.domain.PlayerBean;
import com.faforever.client.fx.FxApplicationThreadExecutor;
import com.faforever.client.fx.JavaFxUtil;
import com.faforever.client.fx.PlatformService;
-import com.faforever.client.fx.SimpleChangeListener;
-import com.faforever.client.fx.SimpleInvalidationListener;
import com.faforever.client.fx.WebViewConfigurer;
import com.faforever.client.i18n.I18n;
import com.faforever.client.navigation.NavigationHandler;
@@ -25,7 +22,6 @@
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
-import javafx.collections.WeakListChangeListener;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
@@ -67,7 +63,6 @@ public class ChannelTabController extends AbstractChatTabController {
private static final int TOPIC_CHARACTERS_LIMIT = 350;
private final PlatformService platformService;
- private final AudioService audioService;
public Tab root;
public SplitPane splitPane;
@@ -91,13 +86,11 @@ public class ChannelTabController extends AbstractChatTabController {
.orElse(
FXCollections.emptyObservableList());
private final ListChangeListener channelUserListChangeListener = this::updateChangedUsersStyles;
- private final WeakListChangeListener weakChannelUserListChangeListener = new WeakListChangeListener<>(
- channelUserListChangeListener);
public ChannelTabController(WebViewConfigurer webViewConfigurer, LoginService loginService, ChatService chatService,
PlayerService playerService,
- AudioService audioService, TimeService timeService, I18n i18n,
+ TimeService timeService, I18n i18n,
NotificationService notificationService, UiService uiService,
NavigationHandler navigationHandler,
CountryFlagService countryFlagService, EmoticonService emoticonService,
@@ -108,7 +101,6 @@ public ChannelTabController(WebViewConfigurer webViewConfigurer, LoginService lo
webViewConfigurer, emoticonService, countryFlagService, chatPrefs, notificationPrefs,
fxApplicationThreadExecutor, navigationHandler);
this.platformService = platformService;
- this.audioService = audioService;
}
@Override
@@ -117,17 +109,12 @@ protected void onInitialize() {
JavaFxUtil.bindManagedToVisible(topicPane, chatUserList, changeTopicTextButton, topicTextField,
cancelChangesTopicTextButton, topicText, topicCharactersLimitLabel,
chatMessageSearchContainer);
- JavaFxUtil.bind(topicCharactersLimitLabel.visibleProperty(), topicTextField.visibleProperty());
- JavaFxUtil.bind(cancelChangesTopicTextButton.visibleProperty(), topicTextField.visibleProperty());
- JavaFxUtil.bind(chatUserList.visibleProperty(), userListVisibilityToggleButton.selectedProperty());
-
- BooleanExpression tabPaneShowing = BooleanExpression.booleanExpression(getRoot().tabPaneProperty()
- .flatMap(
- uiService::createShowingProperty));
- ObservableValue showing = getRoot().selectedProperty()
- .and(tabPaneShowing);
+ topicCharactersLimitLabel.visibleProperty().bind(topicTextField.visibleProperty());
+ cancelChangesTopicTextButton.visibleProperty().bind(topicTextField.visibleProperty());
+ chatUserList.visibleProperty().bind(userListVisibilityToggleButton.selectedProperty());
userListVisibilityToggleButton.selectedProperty().bindBidirectional(chatPrefs.playerListShownProperty());
+
topicTextField.setTextFormatter(new TextFormatter<>(change -> change.getControlNewText()
.length() <= TOPIC_CHARACTERS_LIMIT ? change : null));
@@ -144,10 +131,9 @@ protected void onInitialize() {
.or(topicTextField.visibleProperty())
.when(showing));
- root.idProperty().bind(channelName.when(tabPaneShowing));
- root.textProperty().bind(channelName.map(name -> name.replaceFirst("^#", "")).when(tabPaneShowing));
+ root.textProperty().bind(channelName.map(name -> name.replaceFirst("^#", "")).when(attached));
- chatUserListController.chatChannelProperty().bind(chatChannel);
+ chatUserListController.chatChannelProperty().bind(chatChannel.when(showing));
ObservableValue isModerator = chatChannel.map(channel -> channel.getUser(loginService.getUsername())
.orElse(null))
@@ -159,30 +145,38 @@ protected void onInitialize() {
.and(topicTextField.visibleProperty().not())
.when(showing));
- chatMessageSearchTextField.textProperty().addListener((SimpleChangeListener) this::highlightText);
-
+ chatMessageSearchTextField.textProperty().when(showing).subscribe(this::highlightText);
chatPrefs.hideFoeMessagesProperty()
.when(showing)
- .addListener((SimpleChangeListener) this::hideFoeMessages);
+ .subscribe(this::hideFoeMessages);
chatPrefs.chatColorModeProperty()
.when(showing)
- .addListener((SimpleInvalidationListener) () -> users.getValue().forEach(this::updateUserMessageColor));
+ .subscribe(() -> users.getValue().forEach(this::updateUserMessageColor));
channelTopic.when(showing).subscribe(this::updateChannelTopic);
- users.when(showing).addListener((observable, oldValue, newValue) -> {
+ userListVisibilityToggleButton.selectedProperty().when(showing).subscribe(this::updateDividerPosition);
+
+ users.when(attached).subscribe((oldValue, newValue) -> {
if (oldValue != null) {
- oldValue.removeListener(weakChannelUserListChangeListener);
+ oldValue.removeListener(channelUserListChangeListener);
}
if (newValue != null) {
- newValue.addListener(weakChannelUserListChangeListener);
+ newValue.addListener(channelUserListChangeListener);
}
});
- userListVisibilityToggleButton.selectedProperty().when(showing).subscribe(this::updateDividerPosition);
-
AutoCompletionHelper autoCompletionHelper = getAutoCompletionHelper();
autoCompletionHelper.bindTo(messageTextField());
}
+ @Override
+ public void onDetached() {
+ super.onDetached();
+ ObservableList users = this.users.getValue();
+ if (users != null) {
+ users.removeListener(channelUserListChangeListener);
+ }
+ }
+
public AutoCompletionHelper getAutoCompletionHelper() {
return new AutoCompletionHelper(currentWord -> users.getValue()
.stream()
@@ -345,8 +339,6 @@ protected void onMention(ChatMessage chatMessage) {
.orElse(false)) {
log.debug("Ignored ping from {}", chatMessage.username());
} else if (!hasFocus()) {
- audioService.playChatMentionSound();
- showNotificationIfNecessary(chatMessage);
incrementUnreadMessagesCount();
setUnread(true);
}
diff --git a/src/main/java/com/faforever/client/chat/ChatCategoryItemController.java b/src/main/java/com/faforever/client/chat/ChatCategoryItemController.java
index 8152615aee..4c7c67e7e8 100644
--- a/src/main/java/com/faforever/client/chat/ChatCategoryItemController.java
+++ b/src/main/java/com/faforever/client/chat/ChatCategoryItemController.java
@@ -14,7 +14,6 @@
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
-import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.scene.Node;
@@ -54,8 +53,6 @@ protected void onInitialize() {
}
private void bindProperties() {
- ObservableValue showing = uiService.createShowingProperty(getRoot());
-
categoryLabel.styleProperty()
.bind(chatPrefs.groupToColorProperty()
.flatMap(groupToColor -> chatUserCategory.map(groupToColor::get))
@@ -64,7 +61,7 @@ private void bindProperties() {
.orElse("")
.when(showing));
- categoryLabel.textProperty().bind(chatUserCategory.map(ChatUserCategory::getI18nKey).map(i18n::get));
+ categoryLabel.textProperty().bind(chatUserCategory.map(ChatUserCategory::getI18nKey).map(i18n::get).when(showing));
channelHiddenCategories.bind(Bindings.valueAt(chatPrefs.getChannelNameToHiddenCategories(), channelName)
.orElse(FXCollections.observableSet())
@@ -72,9 +69,9 @@ private void bindProperties() {
arrowLabel.textProperty()
.bind(channelHiddenCategories.flatMap(hiddenCategories -> Bindings.createBooleanBinding(() -> hiddenCategories.contains(chatUserCategory.get()), hiddenCategories, chatUserCategory))
- .map(hidden -> hidden ? "˃" : "˅"));
+ .map(hidden -> hidden ? "˃" : "˅").when(showing));
- userCounterLabel.textProperty().bind(numCategoryItems.map(String::valueOf));
+ userCounterLabel.textProperty().bind(numCategoryItems.map(String::valueOf).when(showing));
}
public void onCategoryClicked(MouseEvent mouseEvent) {
diff --git a/src/main/java/com/faforever/client/chat/ChatChannel.java b/src/main/java/com/faforever/client/chat/ChatChannel.java
index f85ef2bff0..98c2ca18d4 100644
--- a/src/main/java/com/faforever/client/chat/ChatChannel.java
+++ b/src/main/java/com/faforever/client/chat/ChatChannel.java
@@ -2,7 +2,9 @@
import com.faforever.client.fx.JavaFxUtil;
import javafx.beans.Observable;
+import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
@@ -33,6 +35,7 @@ public class ChatChannel {
private final ObjectProperty topic = new SimpleObjectProperty<>(new ChannelTopic("", ""));
private final Set> messageListeners = new HashSet<>();
private final List messages = new ArrayList<>();
+ private final BooleanProperty open = new SimpleBooleanProperty();
private int maxNumMessages = Integer.MAX_VALUE;
@@ -111,4 +114,16 @@ public boolean isPrivateChannel() {
public boolean isPartyChannel() {
return name.endsWith(ChatService.PARTY_CHANNEL_SUFFIX);
}
+
+ public boolean isOpen() {
+ return open.get();
+ }
+
+ public BooleanProperty openProperty() {
+ return open;
+ }
+
+ public void setOpen(boolean open) {
+ this.open.set(open);
+ }
}
diff --git a/src/main/java/com/faforever/client/chat/ChatController.java b/src/main/java/com/faforever/client/chat/ChatController.java
index b431f550df..d084f0a436 100644
--- a/src/main/java/com/faforever/client/chat/ChatController.java
+++ b/src/main/java/com/faforever/client/chat/ChatController.java
@@ -7,7 +7,6 @@
import com.faforever.client.main.event.NavigateEvent;
import com.faforever.client.net.ConnectionState;
import com.faforever.client.theme.UiService;
-import javafx.beans.binding.BooleanExpression;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.WeakListChangeListener;
@@ -63,8 +62,6 @@ public class ChatController extends NodeController {
protected void onInitialize() {
super.onInitialize();
- BooleanExpression showing = uiService.createShowingProperty(getRoot());
-
chatService.addChannelsListener(new WeakMapChangeListener<>(channelChangeListener));
chatService.getChannels().forEach(this::onChannelJoined);
@@ -130,9 +127,10 @@ private void addAndSelectTab(ChatChannel chatChannel) {
}
tabController.setChatChannel(chatChannel);
channelToChatTabController.put(chatChannel, tabController);
-
Tab tab = tabController.getRoot();
tab.setUserData(chatChannel);
+
+
if (chatService.isDefaultChannel(chatChannel)) {
tabPane.getTabs().add(0, tab);
tabPane.getSelectionModel().select(tab);
@@ -141,7 +139,7 @@ private void addAndSelectTab(ChatChannel chatChannel) {
tabPane.getTabs().add(tabPane.getTabs().size() - 1, tab);
if (chatChannel.isPrivateChannel() || tabPane.getSelectionModel().getSelectedIndex() == tabPane.getTabs()
- .size() - 1) {
+ .size() - 1) {
tabPane.getSelectionModel().select(tab);
tabController.onDisplay();
}
@@ -179,7 +177,7 @@ protected void onNavigate(NavigateEvent navigateEvent) {
if (tabPane.getTabs().size() > 1) {
Tab tab = tabPane.getSelectionModel().getSelectedItem();
Optional.ofNullable(channelToChatTabController.get((ChatChannel) tab.getUserData()))
- .ifPresent(AbstractChatTabController::onDisplay);
+ .ifPresent(AbstractChatTabController::onDisplay);
}
}
}
diff --git a/src/main/java/com/faforever/client/chat/ChatService.java b/src/main/java/com/faforever/client/chat/ChatService.java
index 80a3f02eba..04abed7e39 100644
--- a/src/main/java/com/faforever/client/chat/ChatService.java
+++ b/src/main/java/com/faforever/client/chat/ChatService.java
@@ -27,6 +27,10 @@ public interface ChatService {
void removeChannelsListener(MapChangeListener listener);
+ default void leaveChannel(String channelName) {
+ leaveChannel(getOrCreateChannel(channelName));
+ }
+
void leaveChannel(ChatChannel channel);
CompletableFuture sendActionInBackground(ChatChannel chatChannel, String action);
diff --git a/src/main/java/com/faforever/client/chat/ChatUserItemController.java b/src/main/java/com/faforever/client/chat/ChatUserItemController.java
index 014bb46b59..ad02fd577c 100644
--- a/src/main/java/com/faforever/client/chat/ChatUserItemController.java
+++ b/src/main/java/com/faforever/client/chat/ChatUserItemController.java
@@ -112,7 +112,7 @@ private void initializePlayerNoteTooltip() {
noteTooltip = new Tooltip();
noteTooltip.setShowDelay(Duration.ZERO);
noteTooltip.setShowDuration(Duration.seconds(30));
- noteTooltip.textProperty().isEmpty().addListener((observable, oldValue, newValue) -> {
+ noteTooltip.textProperty().isEmpty().when(showing).subscribe((oldValue, newValue) -> {
if (newValue) {
Tooltip.uninstall(userContainer, noteTooltip);
} else {
@@ -140,8 +140,6 @@ private void initializeStatusTooltip() {
}
public void installGameTooltip(GameTooltipController gameInfoController, Tooltip tooltip) {
- ObservableValue showing = uiService.createShowingProperty(getRoot());
-
mapImageView.setOnMouseEntered(event -> gameInfoController.gameProperty()
.bind(chatUser.flatMap(ChatChannelUser::playerProperty).flatMap(PlayerBean::gameProperty).when(showing)));
Tooltip.install(mapImageView, tooltip);
@@ -208,8 +206,6 @@ public ObjectProperty chatUserProperty() {
}
private void bindProperties() {
- ObservableValue showing = uiService.createShowingProperty(getRoot());
-
ObservableValue playerProperty = chatUser.flatMap(ChatChannelUser::playerProperty);
ObservableValue gameProperty = playerProperty.flatMap(PlayerBean::gameProperty);
BooleanExpression gameNotClosedObservable = BooleanExpression.booleanExpression(gameProperty.flatMap(GameBean::statusProperty)
diff --git a/src/main/java/com/faforever/client/chat/ChatUserListController.java b/src/main/java/com/faforever/client/chat/ChatUserListController.java
index f49fd975a9..3550f15c3a 100644
--- a/src/main/java/com/faforever/client/chat/ChatUserListController.java
+++ b/src/main/java/com/faforever/client/chat/ChatUserListController.java
@@ -64,7 +64,10 @@
public class ChatUserListController extends NodeController {
private static final Comparator CHAT_LIST_ITEM_COMPARATOR = Comparator.comparing(ChatListItem::category)
- .thenComparing(ChatListItem::user, Comparator.nullsFirst(Comparator.comparing(ChatChannelUser::getUsername)));
+ .thenComparing(ChatListItem::user,
+ Comparator.nullsFirst(
+ Comparator.comparing(
+ ChatChannelUser::getUsername)));
private final UiService uiService;
private final I18n i18n;
@@ -87,39 +90,48 @@ public class ChatUserListController extends NodeController {
private final ObjectProperty chatChannel = new SimpleObjectProperty<>();
private final ObservableValue> users = chatChannel.map(ChatChannel::getUsers);
private final ObservableValue channelName = chatChannel.map(ChatChannel::getName);
- private final ObservableList unfilteredSource = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
- private final FilteredList items = new FilteredList<>(new SortedList<>(unfilteredSource, CHAT_LIST_ITEM_COMPARATOR));
- private final ObjectProperty> hiddenCategories = new SimpleObjectProperty<>(FXCollections.emptyObservableSet());
+ private final ObservableList unfilteredSource = FXCollections.synchronizedObservableList(
+ FXCollections.observableArrayList());
+ private final FilteredList items = new FilteredList<>(
+ new SortedList<>(unfilteredSource, CHAT_LIST_ITEM_COMPARATOR));
+ private final ObjectProperty> hiddenCategories = new SimpleObjectProperty<>(
+ FXCollections.emptyObservableSet());
private final Map> userChatListItemMap = new ConcurrentHashMap<>();
- private final ObservableValue> hiddenCategoryPredicate = hiddenCategories.flatMap(categories -> Bindings.createObjectBinding(() -> categories.stream()
- .map(category -> (Predicate) item -> item.user() == null || item.category() != category)
- .reduce(item -> true, Predicate::and), categories)).orElse(item -> true);
+ private final ObservableValue> hiddenCategoryPredicate = hiddenCategories.flatMap(
+ categories -> Bindings.createObjectBinding(() -> categories.stream()
+ .map(
+ category -> (Predicate) item -> item.user() == null || item.category() != category)
+ .reduce(item -> true, Predicate::and), categories))
+ .orElse(
+ item -> true);
private final ListChangeListener channelUserListListener = this::onUserChange;
- private final WeakListChangeListener weakUserListChangeListener = new WeakListChangeListener<>(channelUserListListener);
+ private final WeakListChangeListener weakUserListChangeListener = new WeakListChangeListener<>(
+ channelUserListListener);
private ChatUserFilterController chatUserFilterController;
@Override
protected void onInitialize() {
- ObservableValue showing = uiService.createShowingProperty(getRoot());
-
- hiddenCategories.bind(Bindings.valueAt(chatPrefs.getChannelNameToHiddenCategories(), chatChannel.map(ChatChannel::getName))
- .orElse(FXCollections.observableSet())
- .when(showing));
+ hiddenCategories.bind(
+ Bindings.valueAt(chatPrefs.getChannelNameToHiddenCategories(), chatChannel.map(ChatChannel::getName))
+ .orElse(FXCollections.observableSet())
+ .when(showing));
searchUsernameTextField.promptTextProperty()
- .bind(users.flatMap(Bindings::size).map(size -> i18n.get("chat.userCount", size)).when(showing));
+ .bind(users.flatMap(Bindings::size)
+ .map(size -> i18n.get("chat.userCount", size))
+ .when(showing));
users.when(showing).subscribe((oldValue, newValue) -> {
unfilteredSource.removeIf(item -> item.user() != null);
if (oldValue != null) {
- JavaFxUtil.removeListener(oldValue, weakUserListChangeListener);
+ oldValue.removeListener(weakUserListChangeListener);
}
if (newValue != null) {
- JavaFxUtil.addListener(newValue, weakUserListChangeListener);
+ newValue.addListener(weakUserListChangeListener);
List.copyOf(newValue).forEach(this::onUserJoined);
}
@@ -133,10 +145,11 @@ protected void onInitialize() {
for (ChatUserCategory category : ChatUserCategory.values()) {
FilteredList categoryFilteredList = new FilteredList<>(unfilteredSource);
categoryFilteredList.predicateProperty()
- .bind(chatUserFilterController.predicateProperty()
- .map(filterPredicate -> filterPredicate.and(item -> item.user() != null && item.category() == category)));
- fxApplicationThreadExecutor.execute(() -> unfilteredSource.add(new ChatListItem(null, category, channelName, Bindings.size(categoryFilteredList)
- .asObject())));
+ .bind(chatUserFilterController.predicateProperty()
+ .map(filterPredicate -> filterPredicate.and(
+ item -> item.user() != null && item.category() == category)));
+ ChatListItem item = new ChatListItem(null, category, channelName, Bindings.size(categoryFilteredList).asObject());
+ fxApplicationThreadExecutor.execute(() -> unfilteredSource.add(item));
}
}
@@ -170,7 +183,8 @@ private void initializeGameTooltip() {
private void initializeList() {
chatItemListView = VirtualFlow.createVertical(items, this::createCellWithItem, Gravity.FRONT);
- VirtualizedScrollPane>> scrollPane = new VirtualizedScrollPane<>(chatItemListView);
+ VirtualizedScrollPane>> scrollPane = new VirtualizedScrollPane<>(
+ chatItemListView);
scrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
@@ -179,8 +193,8 @@ private void initializeList() {
userListTools.setDisable(false);
items.predicateProperty()
- .bind(chatUserFilterController.predicateProperty()
- .flatMap(filterPredicate -> hiddenCategoryPredicate.map(filterPredicate::and)));
+ .bind(chatUserFilterController.predicateProperty()
+ .flatMap(filterPredicate -> hiddenCategoryPredicate.map(filterPredicate::and)));
}
private void onUserJoined(ChatChannelUser user) {
@@ -214,14 +228,18 @@ private void onUserChange(Change extends ChatChannelUser> change) {
private void initializeFilter() {
chatUserFilterController = uiService.loadFxml("theme/filter/filter.fxml", ChatUserFilterController.class);
- chatUserFilterController.bindExternalFilter(searchUsernameTextField.textProperty(), (text, item) -> text.isEmpty() || item.user() == null || StringUtils.containsIgnoreCase(item.user()
- .getUsername(), text));
+ chatUserFilterController.addExternalFilter(searchUsernameTextField.textProperty().when(showing),
+ (text, item) -> text.isEmpty() || item.user() == null || StringUtils.containsIgnoreCase(
+ item.user().getUsername(), text));
chatUserFilterController.completeSetting();
- filterPopup = PopupUtil.createPopup(PopupWindow.AnchorLocation.CONTENT_TOP_RIGHT, chatUserFilterController.getRoot());
+ filterPopup = PopupUtil.createPopup(PopupWindow.AnchorLocation.CONTENT_TOP_RIGHT,
+ chatUserFilterController.getRoot());
- JavaFxUtil.addAndTriggerListener(chatUserFilterController.filterStateProperty(), (observable, oldValue, newValue) -> filterButton.setSelected(newValue));
- JavaFxUtil.addAndTriggerListener(filterButton.selectedProperty(), observable -> filterButton.setSelected(chatUserFilterController.getFilterState()));
+ chatUserFilterController.filterActiveProperty().when(showing).subscribe(filterButton::setSelected);
+ filterButton.selectedProperty()
+ .when(showing)
+ .subscribe(() -> filterButton.setSelected(chatUserFilterController.getFilterActive()));
}
public void onListCustomizationButtonClicked() {
@@ -248,19 +266,19 @@ public VBox getRoot() {
@VisibleForTesting
List getUserListByCategory(ChatUserCategory category) {
return unfilteredSource.stream()
- .filter(item -> item.category() == category)
- .map(ChatListItem::user)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
+ .filter(item -> item.category() == category)
+ .map(ChatListItem::user)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
}
@VisibleForTesting
List getFilteredUserListByCategory(ChatUserCategory category) {
return items.stream()
- .filter(item -> item.category() == category)
- .map(ChatListItem::user)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
+ .filter(item -> item.category() == category)
+ .map(ChatListItem::user)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
}
@VisibleForTesting
diff --git a/src/main/java/com/faforever/client/chat/KittehChatService.java b/src/main/java/com/faforever/client/chat/KittehChatService.java
index 798e63c705..ba41a0ba8c 100644
--- a/src/main/java/com/faforever/client/chat/KittehChatService.java
+++ b/src/main/java/com/faforever/client/chat/KittehChatService.java
@@ -1,19 +1,27 @@
package com.faforever.client.chat;
+import com.faforever.client.audio.AudioService;
import com.faforever.client.config.ClientProperties;
import com.faforever.client.config.ClientProperties.Irc;
import com.faforever.client.domain.PlayerBean;
import com.faforever.client.fx.FxApplicationThreadExecutor;
import com.faforever.client.fx.JavaFxUtil;
import com.faforever.client.fx.SimpleChangeListener;
+import com.faforever.client.main.event.NavigateEvent;
+import com.faforever.client.main.event.NavigationItem;
+import com.faforever.client.navigation.NavigationHandler;
import com.faforever.client.net.ConnectionState;
+import com.faforever.client.notification.NotificationService;
+import com.faforever.client.notification.TransientNotification;
import com.faforever.client.player.PlayerService;
import com.faforever.client.player.SocialStatus;
import com.faforever.client.preferences.ChatPrefs;
+import com.faforever.client.preferences.NotificationPrefs;
import com.faforever.client.remote.FafServerAccessor;
import com.faforever.client.ui.tray.TrayIconManager;
import com.faforever.client.ui.tray.event.UpdateApplicationBadgeEvent;
import com.faforever.client.user.LoginService;
+import com.faforever.client.util.IdenticonUtil;
import com.faforever.commons.lobby.Player.LeaderboardStats;
import com.faforever.commons.lobby.SocialInfo;
import com.google.common.annotations.VisibleForTesting;
@@ -70,10 +78,14 @@
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.faforever.client.chat.ChatColorMode.RANDOM;
+import static com.faforever.client.player.SocialStatus.FOE;
import static java.util.Locale.US;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static javafx.collections.FXCollections.observableHashMap;
import static javafx.collections.FXCollections.synchronizedObservableMap;
@@ -95,6 +107,10 @@ public class KittehChatService implements ChatService, InitializingBean, Disposa
private final ChatPrefs chatPrefs;
private final FxApplicationThreadExecutor fxApplicationThreadExecutor;
private final TrayIconManager trayIconManager;
+ private final NotificationPrefs notificationPrefs;
+ private final AudioService audioService;
+ private final NotificationService notificationService;
+ private final NavigationHandler navigationHandler;
@Qualifier("userWebClient")
private final ObjectFactory userWebClientFactory;
@@ -102,6 +118,11 @@ public class KittehChatService implements ChatService, InitializingBean, Disposa
* Maps channels by name.
*/
private final ObservableMap channels = synchronizedObservableMap(observableHashMap());
+ /**
+ * A list of channels the server wants us to join.
+ */
+ private final List autoChannels = new ArrayList<>();
+ private final Queue bufferedChannels = new ArrayDeque<>();
@VisibleForTesting
ObjectProperty connectionState = new SimpleObjectProperty<>(ConnectionState.DISCONNECTED);
@VisibleForTesting
@@ -109,11 +130,6 @@ public class KittehChatService implements ChatService, InitializingBean, Disposa
@VisibleForTesting
DefaultClient client;
private String username;
- /**
- * A list of channels the server wants us to join.
- */
- private final List autoChannels = new ArrayList<>();
- private final Queue bufferedChannels = new ArrayDeque<>();
private boolean autoReconnect;
@@ -266,8 +282,61 @@ private void onChannelMessage(ChannelMessageEvent event) {
String channelName = event.getChannel().getName();
- getOrCreateChannel(channelName).addMessage(
- new ChatMessage(Instant.now(), user.getNick(), event.getMessage(), false));
+ String text = event.getMessage();
+ String sender = user.getNick();
+ ChatChannel chatChannel = getOrCreateChannel(channelName);
+ notifyIfMentioned(text, chatChannel, sender);
+
+ chatChannel.addMessage(new ChatMessage(Instant.now(), sender, text, false));
+ }
+
+ private void notifyIfMentioned(String text, ChatChannel chatChannel, String sender) {
+ Matcher matcher = Pattern.compile(
+ "(^|[^A-Za-z0-9-])" + Pattern.quote(loginService.getUsername()) + "([^A-Za-z0-9-]|$)",
+ CASE_INSENSITIVE).matcher(text);
+ boolean mentioned = matcher.find();
+ if (mentioned) {
+ boolean fromFoe = playerService.getPlayerByNameIfOnline(sender)
+ .map(PlayerBean::getSocialStatus)
+ .map(FOE::equals)
+ .orElse(false);
+ if (fromFoe || (notificationPrefs.getNotifyOnAtMentionOnlyEnabled() && !text.contains(
+ "@" + loginService.getUsername()))) {
+ log.debug("Ignored ping {} from {}", text, sender);
+ return;
+ }
+
+ audioService.playChatMentionSound();
+
+ String identIconSource = playerService.getPlayerByNameIfOnline(sender)
+ .map(PlayerBean::getId)
+ .map(String::valueOf)
+ .orElse(sender);
+
+ if (!chatChannel.isOpen() && notificationPrefs.isPrivateMessageToastEnabled()) {
+ notificationService.addNotification(
+ new TransientNotification(sender, text, IdenticonUtil.createIdenticon(identIconSource), evt -> {
+ navigationHandler.navigateTo(new NavigateEvent(NavigationItem.CHAT));
+ }));
+ }
+ }
+ }
+
+ private void notifyOnPrivateMessage(String text, ChatChannel chatChannel, String sender) {
+ if (chatChannel.isPrivateChannel() && !chatChannel.isOpen()) {
+ audioService.playPrivateMessageSound();
+
+ if (!chatChannel.isOpen() && notificationPrefs.isPrivateMessageToastEnabled()) {
+ String identIconSource = playerService.getPlayerByNameIfOnline(sender)
+ .map(PlayerBean::getId)
+ .map(String::valueOf)
+ .orElse(sender);
+ notificationService.addNotification(
+ new TransientNotification(sender, text, IdenticonUtil.createIdenticon(identIconSource), evt -> {
+ navigationHandler.navigateTo(new NavigateEvent(NavigationItem.CHAT));
+ }));
+ }
+ }
}
@Handler
@@ -303,16 +372,21 @@ private void onPrivateMessage(PrivateMessageEvent event) {
User user = event.getActor();
ircLog.debug("Received private message: {}", event);
- ChatChannelUser sender = getOrCreateChatUser(user.getNick(), user.getNick());
- if (sender.getPlayer()
+ String senderNick = user.getNick();
+
+ if (playerService.getPlayerByNameIfOnline(senderNick)
.map(PlayerBean::getSocialStatus)
.map(status -> status == SocialStatus.FOE)
.orElse(false) && chatPrefs.isHideFoeMessages()) {
- ircLog.debug("Suppressing chat message from foe '{}'", user.getNick());
+ ircLog.debug("Suppressing chat message from foe '{}'", senderNick);
return;
}
- getOrCreateChannel(user.getNick()).addMessage(new ChatMessage(Instant.now(), user.getNick(), event.getMessage()));
+ String text = event.getMessage();
+ ChatChannel chatChannel = getOrCreateChannel(senderNick);
+ notifyOnPrivateMessage(text, chatChannel, senderNick);
+
+ chatChannel.addMessage(new ChatMessage(Instant.now(), senderNick, text));
}
private void joinAutoChannels() {
diff --git a/src/main/java/com/faforever/client/chat/MatchmakingChatController.java b/src/main/java/com/faforever/client/chat/MatchmakingChatController.java
index 280a0e7ebb..6ba990e760 100644
--- a/src/main/java/com/faforever/client/chat/MatchmakingChatController.java
+++ b/src/main/java/com/faforever/client/chat/MatchmakingChatController.java
@@ -15,8 +15,6 @@
import com.faforever.client.user.LoginService;
import com.faforever.client.util.TimeService;
import com.google.common.annotations.VisibleForTesting;
-import javafx.beans.binding.BooleanExpression;
-import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
@@ -77,14 +75,9 @@ public MatchmakingChatController(LoginService loginService,
protected void onInitialize() {
super.onInitialize();
- ObservableValue showing = getRoot().selectedProperty()
- .and(BooleanExpression.booleanExpression(getRoot().tabPaneProperty()
- .flatMap(uiService::createShowingProperty)));
-
- matchmakingChatTabRoot.idProperty().bind(channelName.when(showing));
matchmakingChatTabRoot.textProperty().bind(channelName.when(showing));
- chatChannel.addListener(((observable, oldValue, newValue) -> {
+ chatChannel.when(attached).addListener(((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeUserListener(usersChangeListener);
}
@@ -95,15 +88,24 @@ protected void onInitialize() {
}));
String topic = i18n.get("teammatchmaking.chat.topic");
- topicText.getChildren().clear();
- Arrays.stream(topic.split("\\s")).forEach(word -> {
+ List