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.
 
 
 

383 lines
12 KiB

package wow.doge.chatto.controller
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.control.Button
import javafx.scene.control.TextArea
import javafx.scene.layout.HBox
import javafx.scene.layout.VBox
import scalafx.Includes._
import javax.inject.Inject
import org.scalafx.extras._
import wow.doge.chatto.service.UserService
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 scalafx.collections.ObservableBuffer
import com.sfxcode.sapphire.core.value.KeyBindings
import com.sfxcode.sapphire.core.value.FXBeanAdapter
import wow.doge.chatto.service.ActiveUser
import scala.collection.concurrent.TrieMap
import javafx.beans.property.ReadOnlyListProperty
import scalafx.beans.property.BufferProperty
import javafx.collections.FXCollections
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Priority
import net.synedra.validatorfx.Validator
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 wow.doge.chatto.control.MessageBox
import javafx.scene.control.SelectionMode
import javafx.scene.control.ListCell
import java.time.Instant
import com.github.marlonlom.utilities.timeago.TimeAgo
import wow.doge.chatto.service.EncryptionService
import wow.doge.chatto.model.MessageCipher
import wow.doge.chatto.model.MessageType
import scala.util.Try
import java.time.ZonedDateTime
import wow.doge.chatto.model.Message
class ChatController @Inject() (
userService: UserService,
encryptionService: EncryptionService,
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[ActiveUser] = _
@FXML var chatListView: JFXListView[Message] = _
@FXML private var curUsr: Label = _
@FXML private var lastActiveLabel: Label = _
@FXML private var isOnlineLabel: Label = _
@FXML private var selectedUserBox: HBox = _
private lazy val usersBuffer = ObservableBuffer.empty[ActiveUser]
private lazy 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
*/
lazy val usersListROProp: ReadOnlyListProperty[ActiveUser] = BufferProperty(
FXCollections.unmodifiableObservableList(usersListProperty())
)
private lazy 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
chatListView.setCellFactory(_ =>
new ChatListCell(appDataHandler.appData.credentials.username)
)
chatDataAdapter.set(FXBean(ChatData.empty))
Array(submitButton, chatInput).foreach(n => {
n.disableProperty() <== usersListView
.selectionModel()
.selectedItemProperty()
.isNull()
})
// usersListView2.setCellFactory(lv => {})
// 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.cellFactory = _ => {
new ListCell[ActiveUser] {
override def updateItem(item: ActiveUser, empty: Boolean): Unit = {
super.updateItem(item, empty)
if (empty || item == null) {
setText("")
} else {
setText(item.userName)
}
}
}
}
usersListView.items <== usersListProperty
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 => {
cdp.messages().clear()
lastActiveLabel.text <== cdp.lastActive
isOnlineLabel.text <== cdp.isActive.asString()
// chatListView.items <== cdp.messages
// logger.debug(s"1 ${cdp.messages}")
})
async {
val maybeMessages = await {
offFXAndWait {
userService
.getMessages(appDataHandler.appData.credentials, nv.userName)
.map(_.body)
}
}
logger.debug(maybeMessages.toString)
onFX {
// maybeMessages.foreach(
// _.map(m =>
// maybeCDP.foreach(cdp => {
// cdp.messages() ++= m
// // logger.debug(
// // s"2 ${chatDataStore.get(nv.userName).map(_.messages())}"
// // )
// chatListView.items <== cdp.messages
// })
// )
// )
for {
tryMessages <- maybeMessages
messages <- tryMessages.toEither
cdp <- maybeCDP.toRight("CDP is null")
_ <- Right {
cdp.messages ++= messages
chatListView.items <== cdp.messages
}
} yield ()
}
}
})
})
val copyMessageMenuItem = new MenuItem("Copy Message")
copyMessageMenuItem.accelerator =
new KeyCodeCombination(KeyCode.C, KeyCombination.ControlDown)
copyMessageMenuItem.onAction = _ => {
val content = new ClipboardContent()
val messages = chatListView.selectionModel().selectedItems.map(_.message)
content.putString(messages.mkString("\n"))
Clipboard.getSystemClipboard().setContent(content)
}
val chatListMenu = new ContextMenu()
chatListMenu.items += copyMessageMenuItem
chatListView.contextMenu = chatListMenu
submitButton.onAction = (e) => {
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()
// )
cdp.messages += Message(
fromUser = appDataHandler.appData.credentials.username,
toUser = chatDataAdapter.get.bean.userName,
chatInput.text(),
Instant.now()
)
// Message.empty.copy(message = chatInput.text()) +=: cdp.messages()
})
}
}
val validator = new Validator()
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")
maybeActiveUsers.foreach(users => {
users.foreach(user => {
val chatData =
ChatData(user.userName, user, ObservableBuffer.empty[Message])
chatDataStore.put(user.userName, new ChatDataProperty(chatData))
})
})
onFX {
maybeActiveUsers.foreach(users => usersBuffer ++= users)
}
// chatDataStore
// .map { case (key, value) => value }
// .foreach(cdp => {
// cdp.messages ++= Seq(
// Message.empty.copy(message = "hi"),
// Message.empty.copy(message = "hello"),
// Message.empty.copy(message = "bye")
// )
// })
// simulate update
val maybeCDP = for {
usersMap <- maybeActiveUsers.map(_.groupBy(_.userName))
user <- usersMap.get("user1").toRight("")
cdp <- chatDataStore.get("user1").toRight("")
} yield (cdp)
maybeCDP.foreach(cdp => {
cdp.isActive() = true
})
}
}
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.foreach {
// case (_, cdp) => cdp.messages.clear()
// }
chatDataStore.clear()
usersBuffer.clear()
chatInput.clear()
this.stage.maximized = false
applicationController.logout()
}
}
final case class ChatData(
userName: String,
activeUser: ActiveUser,
messages: ObservableBuffer[Message]
) {
lazy val lastActiveString =
activeUser.lastActive
.map(time => TimeAgo.using(time.toInstant().toEpochMilli()))
.getOrElse("User has not logged in yet")
lazy val onlineString = activeUser.online.toString()
}
final object ChatData {
def empty = {
ChatData("empty", ActiveUser.empty, ObservableBuffer.empty[Message])
}
}
final class ChatDataProperty(chatData: ChatData) {
val bean = FXBean(chatData)
val username = bean.getStringProperty("userName")
val isActive = bean.getBooleanProperty("activeUser.online")
val lastActive = bean.getStringProperty("lastActiveString")
val messages = BufferProperty(chatData.messages)
def updateItem(chatData: ChatData) = {
username() = chatData.userName
isActive() = chatData.activeUser.online
lastActive() = chatData.lastActiveString
messages() ++= chatData.messages
}
}
final case class EncryptedMessage(
fromUser: String,
toUser: String,
messageCipher: MessageCipher,
messageTime: ZonedDateTime
) {
def toMessage(
passphrase: String,
decryptionFn: (String, MessageCipher) => Try[String]
): Try[Message] =
decryptionFn(passphrase, this.messageCipher)
.map(ms => Message(this.fromUser, this.toUser, ms, messageTime.toInstant))
}
final class ChatListCell(principal: String) extends ListCell[Message] {
private val messageBox = new MessageBox()
override def updateItem(item: Message, empty: Boolean): Unit = {
super.updateItem(item, empty)
if (empty || item == null) {
setText(null)
setGraphic(null)
} else {
// messageBox.setItem(item)
// setGraphic(messageBox)
if (principal.equals(item.fromUser)) {
setGraphic(Message.createMdMessageBox(item, MessageType.Sender))
} else {
setGraphic(Message.createMdMessageBox(item, MessageType.Receiver))
}
}
}
}