WIP desktop client for Chatto reimplemented in ScalaFX and Sapphire Framework
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

412 lines
14 KiB

package wow.doge.chatto.controller
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.control.Button
import javafx.scene.layout.FlowPane
import javafx.scene.control.TextArea
import javafx.scene.control.ListView
import javafx.scene.layout.HBox
import javafx.scene.layout.VBox
import scalafx.Includes._
import wow.doge.chatto.control.UserBox
import javafx.application.Platform
import javax.inject.Inject
import org.scalafx.extras._
import wow.doge.chatto.messagebuble.BubbledMDFXNode
import wow.doge.chatto.service.UserService
import scala.concurrent.ExecutionContext
import scala.concurrent.ExecutionContext.Implicits.global
import com.typesafe.scalalogging.LazyLogging
import com.sfxcode.sapphire.core.value.FXBean
import wow.doge.chatto.AppDataHandler
import com.jfoenix.controls.JFXListView
import scala.async.Async.{async, await}
import javafx.scene.paint.Color
import scalafx.collections.ObservableBuffer
import javafx.beans.property.SimpleListProperty
import wow.doge.chatto.control.UserBox2
import javafx.beans.value.ChangeListener
import com.sfxcode.sapphire.core.value.KeyBindings
import com.sfxcode.sapphire.core.value.FXBeanAdapter
import scalafx.collections.ObservableMap
import com.sfxcode.sapphire.core.value.BeanConversions
import javafx.util.converter.DateStringConverter
import javafx.beans.binding.Bindings
import wow.doge.chatto.service.ActiveUser
import scala.collection.mutable
import scala.collection.concurrent.TrieMap
import wow.doge.chatto.messagebuble.BubbleSpec
import javafx.scene.layout.Background
import javafx.scene.layout.BackgroundFill
import javafx.geometry.Pos
import scalafx.beans.property.ReadOnlyBufferProperty
import scalafx.beans.property.ReadOnlyBufferWrapper
import javafx.beans.property.ReadOnlyListProperty
import scalafx.beans.property.BufferProperty
import javafx.collections.FXCollections
import com.sandec.mdfx.MDFXNode
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Priority
import net.synedra.validatorfx.Validator
import wow.doge.chatto.control.JFXSmoothScroll
import javafx.scene.control.ContextMenu
import javafx.scene.control.MenuItem
import javafx.scene.input.Clipboard
import javafx.scene.input.ClipboardContent
import scalafx.scene.input.KeyCodeCombination
import scalafx.scene.input.KeyCode
import scalafx.scene.input.KeyCombination
import javafx.scene.input.DataFormat
import wow.doge.chatto.control.MessageBox
import javafx.scene.control.SelectionMode
import scalafx.beans.property.BooleanProperty
class ChatController @Inject() (
userService: UserService,
appDataHandler: AppDataHandler
) extends AbstractViewController
with LazyLogging {
// @FXML private var label: Label = _
@FXML var chatMainPane: BorderPane = _
// @FXML private var flowPane: FlowPane = _
@FXML private var submitButton: Button = _
@FXML private var logoutButton: Button = _
// @FXML private var chatTextArea: TextArea = _
@FXML private var chatInput: TextArea = _
@FXML private var usersVBox: VBox = _
@FXML var usersListView: JFXListView[UserBox2] = _
@FXML var chatListView: JFXListView[MessageBox] = _
@FXML private var curUsr: Label = _
@FXML private var lastActiveLabel: Label = _
@FXML private var isOnlineLabel: Label = _
@FXML private var selectedUserBox: HBox = _
private val usersBuffer = ObservableBuffer.empty[UserBox2]
private val usersListProperty = BufferProperty(usersBuffer)
/**
* Readonly property wrapping an unmodifiable list.
* Synchronized with the internal users list property.
* Attemping to modify the internal list will throw an exception
*/
val usersListROProp: ReadOnlyListProperty[UserBox2] = BufferProperty(
FXCollections.unmodifiableObservableList(usersListProperty())
)
private val chatDataStore = TrieMap.empty[String, ChatDataProperty]
private lazy val chatDataAdapter = FXBeanAdapter[ChatData](this)
override def didGainVisibilityFirstTime(): Unit = {
super.didGainVisibilityFirstTime()
this.stage.resizable = true
chatMainPane.hgrow = Priority.ALWAYS
chatListView.selectionModel().selectionMode = SelectionMode.MULTIPLE
chatDataAdapter.set(FXBean(ChatData.empty))
Array(submitButton, chatInput).foreach(n => {
n.disableProperty() <== usersListView
.selectionModel()
.selectedItemProperty()
.isNull()
})
// curUserKeys.add("content", "${_self.data().content()}")
val chatDataAdapterKeys = KeyBindings()
chatDataAdapterKeys.add("currentUser", "${_self.userName()}")
// chatDataAdapterKeys.add(
// "lastActive",
// "${_self.lastActiveString()}"
// )
// chatDataAdapterKeys.add(
// "online",
// "${_self.onlineString()}"
// )
// curUserAdapter.addDateConverter()
// curUserAdapter.addBindings(curUserKeys)
chatDataAdapter.addBindings(chatDataAdapterKeys)
usersListView
.selectionModel()
.selectedItemProperty()
.addListener((_, _, newValue) => {
Option(newValue).foreach(nv => {
val maybeCDP = chatDataStore
.get(nv.username)
val chatDataBean = maybeCDP
.map(_.bean)
.getOrElse {
logger.error("Error null")
FXBean(ChatData.empty)
}
chatDataAdapter.set(chatDataBean)
maybeCDP.foreach(cdp => {
// lastActiveLabel.text <== cdp.lastActive
isOnlineLabel.text <== cdp.isActive.asString()
chatListView.items <== cdp.messageBubbles
})
})
})
val copyMessageMenuItem = new MenuItem("Copy Message")
copyMessageMenuItem.accelerator =
new KeyCodeCombination(KeyCode.C, KeyCombination.ControlDown)
copyMessageMenuItem.onAction = _ => {
val content = new ClipboardContent()
val x = chatListView.getSelectionModel().getSelectedItems().map(_.message)
// chatListView.selectionModel().selectedItem().message
Option(x).foreach(message => {
content.putString(message.mkString("\n"))
Clipboard.getSystemClipboard().setContent(content)
})
// val message = cdp.messageList(selectedIndex)
// val maybeCDP = chatDataStore.get(chatDataAdapter.get.bean.userName)
// maybeCDP.foreach(cdp => {
// // val message = Option(cdp.messageList().get(selectedIndex)).toRight {
// // "Unexpected error - message not found"
// // }
// // message.map(msg => {
// // // content.putString(msg)
// // // clipboard.setContent(content)
// // // val content = clipboard.content
// // // content.putString(msg)
// // // clipboard.content = content
// // clipboard.putString(msg)
// // })
// // message.left.map(err => logger.error(err))
// })
}
val chatListMenu = new ContextMenu()
chatListMenu.items += copyMessageMenuItem
chatListView.contextMenu = chatListMenu
usersListView.items <== usersListProperty
val validator = new Validator()
submitButton.disable <== validator.containsErrorsProperty()
submitButton.onAction = (e) => {
// val msgBox = ChatDataProperty.createMdMessageBox2(chatInput.text())
// chatListView.items() += msgBox
if (!chatInput.text().equals("") &&
!chatInput.text().equals(" ") &&
!chatInput.text().equals("\n")) {
val maybeCDP = chatDataStore.get(chatDataAdapter.get.bean.userName)
maybeCDP.foreach(cdp => {
cdp.messageBubbles += ChatDataProperty.createMdMessageBox3(
appDataHandler.appData.credentials.username,
cdp.username(),
chatInput.text()
)
})
}
}
validator
.createCheck()
.withMethod(c => {
val userName = chatInput.text()
if (!userName.toLowerCase().equals(userName)) {
c.error("Please use only lowercase letters.")
}
})
.dependsOn("chatInput", chatInput.text)
.decorates(chatInput)
.immediate()
}
override def didGainVisibility(): Unit = {
super.didGainVisibility()
chatInput.requestFocus()
async {
val willBeActiveUsers = userService
.getActiveUsers(appDataHandler.appData.credentials)
.map(_.body)
val maybeActiveUsers = await(willBeActiveUsers)
logger.debug(s"Received Users: $maybeActiveUsers")
val maybeUserBoxes = maybeActiveUsers.map(users => {
users.map(user => {
val chatData =
ChatData(user.userName, user, ObservableBuffer.empty[String])
chatDataStore.put(user.userName, new ChatDataProperty(chatData))
new UserBox2(user.userName, chatData) {
this.styleClass ++= Seq("text-white")
}
})
})
val messageBox = ChatDataProperty.createMdMessageBox2(
"""**Hello world qefwew yeeehay bwergqwevqcqe**
|**Hello world qefwew yeeehay bwergqwevqcqe**
|
| Hello World
""".stripMargin
)
onFX {
maybeUserBoxes.foreach(userBoxes => {
usersBuffer ++= userBoxes
})
chatListView.items() ++= Seq(
messageBox,
ChatDataProperty.createMdMessageBox2("hello"),
ChatDataProperty.createMdMessageBox2(
""" 1. Hello world qefwew yeeehay bwergqwevqcqe
|1. Hello world qefwew yeeehay bwergqwevqcqe
|1. Hello world qefwew yeeehay bwergqwevqcqe
|1. Hello world qefwew yeeehay bwergqwevqcqe""".stripMargin
)
)
// .map(node => {
// node.prefWidthProperty <== (chatListView.prefWidthProperty - 200)
// node
// })
// JFXSmoothScroll.smoothScrollingListView(chatListView, 0.1)
}
chatDataStore
.map { case (key, value) => value }
.foreach(cdp => {
cdp.messageBubbles ++= Seq(
ChatDataProperty.createMdMessageBox2("hi"),
ChatDataProperty.createMdMessageBox2("hello"),
ChatDataProperty.createMdMessageBox2("bye")
)
// .map(ChatDataProperty.createMdMessageBox)
})
}
}
def func() = {
val x = offFXAndWait {
2 + 3
}
x
}
def actionLogout = onFX {
offFXAndWait {
appDataHandler.clearCredentials()
}
// curUserAdapter.set(User.empty)
logger.debug(s"Logout - clearing credentials - ${appDataHandler.appData}")
chatDataAdapter.set(FXBean(ChatData.empty))
usersListView.items().clear()
chatListView.items().clear()
chatDataStore.clear()
usersBuffer.clear()
chatInput.clear()
this.stage.maximized = false
applicationController.logout()
}
implicit class MyClipboardExtension(clipboard: Clipboard) {
def putString(string: String) = {
// val content = Option(clipboard.getContent(DataFormat.PLAIN_TEXT))
// .getOrElse(new ClipboardContent())
// content.putString(string)
// clipboard.setContent(content)
}
}
}
final case class ChatData(
userName: String,
activeUser: ActiveUser,
messages: ObservableBuffer[String]
) {
lazy val lastActiveString =
activeUser.lastActive
.map(_.toString())
.getOrElse("User has not logged in yet")
lazy val onlineString = activeUser.online.toString()
}
object ChatData {
def empty = {
ChatData("empty", ActiveUser.empty, ObservableBuffer.empty[String])
}
}
class ChatDataProperty(chatData: ChatData) {
import ChatDataProperty._
val bean = FXBean(chatData)
val username = bean.getStringProperty("userName")
val isActive = bean.getBooleanProperty("activeUser.online")
val lastActive = bean.getStringProperty("lastActiveString")
lazy val messageBubbles = BufferProperty(
chatData.messages.map(ChatDataProperty.createMdMessageBox2)
)
def messageList = messageBubbles().map(_.message)
// lazy val messages = messagesBubbleProperty
// .get()
// .map(_.messageText)
}
object ChatDataProperty {
lazy val markdownStyleSheet =
getClass().getResource("/styles/markdown.css").toExternalForm()
def createMdMessageBox(mdfxText: String) = {
val mdfxNode = new BubbledMDFXNode(mdfxText);
mdfxNode
.getStylesheets()
.add(markdownStyleSheet);
mdfxNode.setBubbleSpec(BubbleSpec.FACE_RIGHT_CENTER);
mdfxNode.setBackground(
new Background(new BackgroundFill(Color.LIGHTSTEELBLUE, null, null))
);
val box = new HBox();
mdfxNode.setMinWidth(100.0);
box.setAlignment(Pos.TOP_RIGHT);
box.children += mdfxNode;
box
}
def createMdMessageBox2(mdfxText: String) = {
val mdfxNode = new MDFXNode(mdfxText);
mdfxNode
.getStylesheets()
.add(markdownStyleSheet)
mdfxNode.setMaxWidth(500)
mdfxNode.vgrow = Priority.ALWAYS
mdfxNode.setAlignment(Pos.CENTER)
mdfxNode.styleClass = Seq("chat-message-box")
val box = new MessageBox("", "", mdfxText)
box.setAlignment(Pos.CENTER_RIGHT)
// box.maxWidth(500)
box.hgrow = Priority.ALWAYS
box.vgrow = Priority.ALWAYS
box.children ++= Seq(mdfxNode)
box.fillHeight = true
box
}
def createMdMessageBox3(
sender: String,
receiver: String,
mdfxText: String
) = {
val mdfxNode = new MDFXNode(mdfxText);
mdfxNode
.getStylesheets()
.add(markdownStyleSheet)
mdfxNode.setMaxWidth(500)
mdfxNode.vgrow = Priority.ALWAYS
mdfxNode.setAlignment(Pos.CENTER)
mdfxNode.styleClass = Seq("chat-message-box")
val box = new MessageBox(sender, receiver, mdfxText)
box.setAlignment(Pos.CENTER_RIGHT)
// box.maxWidth(500)
box.hgrow = Priority.ALWAYS
box.vgrow = Priority.ALWAYS
box.children ++= Seq(mdfxNode)
box.fillHeight = true
box
}
}