Code Sketch


yiiii
By: Mhalsakant School
Category: Programming
import javax.swing._
import java.awt._
import java.awt.event._
import java.io._
import java.security.MessageDigest
import scala.collection.mutable.ArrayBuffer
import scala.jdk.CollectionConverters._

val frame = new JFrame("WhatsApp Live Chat - Kojo Edition")

frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setSize(850, 650)
frame.setLocationRelativeTo(null)

case class Message(sender: String, text: String, time: String)
case class UserAccount(username: String, passwordHash: String)

val accountsFile =
  new File(System.getProperty("user.home"), ".whatsapp_kojo_accounts.dat")

val messagesFile =
  new File(System.getProperty("user.home"), ".whatsapp_kojo_messages.dat")

var users = ArrayBuffer.empty[UserAccount]
var messages = ArrayBuffer.empty[Message]

var loggedInUser: Option[String] = None

def hashPassword(password: String): String = {
  val md = MessageDigest.getInstance("SHA-256")
  md.digest(password.getBytes("UTF-8"))
    .map(b => f"$b%02x")
    .mkString
}

def loadData(): Unit = {

  // Load accounts
  try {
    if (accountsFile.exists()) {
      val ois = new ObjectInputStream(
        new FileInputStream(accountsFile)
      )

      try {
        val list =
          ois.readObject().asInstanceOf[java.util.ArrayList[(String, String)]]

        users.clear()

        for ((u, p) <- list.asScala) {
          users += UserAccount(u, p)
        }
      } finally {
        ois.close()
      }
    }
  } catch {
    case _: Throwable =>
      // Ignore corrupted/missing account data
  }

  // Load messages
  try {
    if (messagesFile.exists()) {
      val ois = new ObjectInputStream(
        new FileInputStream(messagesFile)
      )

      try {
        val list =
          ois.readObject()
            .asInstanceOf[java.util.ArrayList[(String, String, String)]]

        messages.clear()

        for ((s, t, tm) <- list.asScala) {
          messages += Message(s, t, tm)
        }
      } finally {
        ois.close()
      }
    }
  } catch {
    case _: Throwable =>
      // Ignore corrupted/missing message data
  }
}

def saveData(): Unit = {
  try {

    val oosAcc =
      new ObjectOutputStream(new FileOutputStream(accountsFile))

    try {
      val accList =
        new java.util.ArrayList[(String, String)]()

      for (u <- users) {
        accList.add((u.username, u.passwordHash))
      }

      oosAcc.writeObject(accList)
    } finally {
      oosAcc.close()
    }

    val oosMsg =
      new ObjectOutputStream(new FileOutputStream(messagesFile))

    try {
      val msgList =
        new java.util.ArrayList[(String, String, String)]()

      for (m <- messages) {
        msgList.add((m.sender, m.text, m.time))
      }

      oosMsg.writeObject(msgList)
    } finally {
      oosMsg.close()
    }

  } catch {
    case ex: Throwable =>
      ex.printStackTrace()
  }
}

// ------------------------------------------------------------
// Main UI
// ------------------------------------------------------------

val cardLayout = new CardLayout()
val mainPanel = new JPanel(cardLayout)

// ------------------------------------------------------------
// Login panel
// ------------------------------------------------------------

val loginPanel = new JPanel(new GridBagLayout())
loginPanel.setBackground(new Color(0, 150, 136))

val gbc = new GridBagConstraints()
gbc.insets = new Insets(10, 10, 10, 10)

gbc.gridx = 0
gbc.gridy = 0
gbc.gridwidth = 2

val titleLabel = new JLabel("WhatsApp Live Chat")
titleLabel.setFont(new Font("Arial", Font.BOLD, 26))
titleLabel.setForeground(Color.WHITE)

loginPanel.add(titleLabel, gbc)

gbc.gridwidth = 1
gbc.gridy = 1

val userLabel = new JLabel("Username:")
userLabel.setForeground(Color.WHITE)

loginPanel.add(userLabel, gbc)

gbc.gridx = 1

val userText = new JTextField(15)
loginPanel.add(userText, gbc)

gbc.gridx = 0
gbc.gridy = 2

val passLabel = new JLabel("Password:")
passLabel.setForeground(Color.WHITE)

loginPanel.add(passLabel, gbc)

gbc.gridx = 1

val passText = new JPasswordField(15)
loginPanel.add(passText, gbc)

gbc.gridx = 0
gbc.gridy = 3

val loginButton = new JButton("Login / Register")
loginPanel.add(loginButton, gbc)

gbc.gridx = 1

val changePassButton = new JButton("Change Password")
loginPanel.add(changePassButton, gbc)

val statusLabel = new JLabel("")
statusLabel.setForeground(Color.YELLOW)

gbc.gridx = 0
gbc.gridy = 4
gbc.gridwidth = 2

loginPanel.add(statusLabel, gbc)

// ------------------------------------------------------------
// Chat panel
// ------------------------------------------------------------

val chatPanel = new JPanel(new BorderLayout())

val topPanel = new JPanel(
  new FlowLayout(FlowLayout.LEFT)
)

topPanel.setBackground(new Color(0, 128, 105))

val headerLabel =
  new JLabel("WhatsApp Live Chat Room")

headerLabel.setFont(
  new Font("Arial", Font.BOLD, 18)
)

headerLabel.setForeground(Color.WHITE)

topPanel.add(headerLabel)

val logoutButton = new JButton("Logout")
topPanel.add(logoutButton)

chatPanel.add(topPanel, BorderLayout.NORTH)

val chatArea = new JTextArea()
chatArea.setEditable(false)
chatArea.setFont(
  new Font("Arial", Font.PLAIN, 14)
)

val scrollPane = new JScrollPane(chatArea)
chatPanel.add(scrollPane, BorderLayout.CENTER)

val bottomPanel = new JPanel(
  new BorderLayout()
)

val messageText = new JTextField()
val sendButton = new JButton("Send")

bottomPanel.add(
  messageText,
  BorderLayout.CENTER
)

bottomPanel.add(
  sendButton,
  BorderLayout.EAST
)

chatPanel.add(
  bottomPanel,
  BorderLayout.SOUTH
)

// ------------------------------------------------------------
// Message display
// ------------------------------------------------------------

def refreshMessages(): Unit = {

  val sb = new StringBuilder()

  for (m <- messages) {
    sb.append(
      s"[${m.time}] ${m.sender}: ${m.text}\n"
    )
  }

  chatArea.setText(sb.toString())

  chatArea.setCaretPosition(
    chatArea.getDocument.getLength
  )
}

// ------------------------------------------------------------
// Login / Register
// ------------------------------------------------------------

loginButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      val u = userText.getText.trim
      val p = new String(passText.getPassword)

      if (u.isEmpty || p.isEmpty) {

        statusLabel.setText(
          "Please enter username and password."
        )

      } else {

        val hashed = hashPassword(p)

        val existing =
          users.find(_.username == u)

        existing match {

          case Some(acc) =>

            if (acc.passwordHash == hashed) {

              loggedInUser = Some(u)

              statusLabel.setText(
                "Login successful!"
              )

              cardLayout.show(
                mainPanel,
                "CHAT"
              )

              refreshMessages()

            } else {

              statusLabel.setText(
                "Incorrect password."
              )
            }

          case None =>

            users += UserAccount(
              u,
              hashed
            )

            saveData()

            loggedInUser = Some(u)

            statusLabel.setText(
              "Account created & logged in!"
            )

            cardLayout.show(
              mainPanel,
              "CHAT"
            )

            refreshMessages()
        }
      }
    }
  }
)

// ------------------------------------------------------------
// Change password
// ------------------------------------------------------------

changePassButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      val u = userText.getText.trim
      val p = new String(passText.getPassword)

      if (u.isEmpty || p.isEmpty) {

        statusLabel.setText(
          "Enter username and new password to change."
        )

      } else {

        val hashed = hashPassword(p)

        val idx =
          users.indexWhere(_.username == u)

        if (idx >= 0) {

          users(idx) =
            UserAccount(u, hashed)

          saveData()

          statusLabel.setText(
            "Password changed successfully!"
          )

        } else {

          statusLabel.setText(
            "User not found to change password."
          )
        }
      }
    }
  }
)

// ------------------------------------------------------------
// Send message
// ------------------------------------------------------------

sendButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      val txt =
        messageText.getText.trim

      if (txt.nonEmpty) {

        loggedInUser match {

          case Some(usr) =>

            val timeStr =
              java.time.LocalTime
                .now()
                .toString
                .take(8)

            messages += Message(
              usr,
              txt,
              timeStr
            )

            saveData()

            messageText.setText("")

            refreshMessages()

          case None =>
            // User is not logged in
        }
      }
    }
  }
)

// Press Enter to send
messageText.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      sendButton.doClick()
    }
  }
)

// ------------------------------------------------------------
// Logout
// ------------------------------------------------------------

logoutButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      loggedInUser = None

      cardLayout.show(
        mainPanel,
        "LOGIN"
      )

      userText.requestFocusInWindow()
    }
  }
)

// ------------------------------------------------------------
// Refresh messages every second
// ------------------------------------------------------------

val timer =
  new javax.swing.Timer(
    1000,
    new ActionListener {

      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        loadData()
        refreshMessages()
      }
    }
  )

timer.start()

// ------------------------------------------------------------
// Finish setup
// ------------------------------------------------------------

mainPanel.add(
  loginPanel,
  "LOGIN"
)

mainPanel.add(
  chatPanel,
  "CHAT"
)

frame.setContentPane(mainPanel)

cardLayout.show(
  mainPanel,
  "LOGIN"
)

loadData()

frame.setVisible(true)

SwingUtilities.invokeLater(
  new Runnable {
    override def run(): Unit =
      userText.requestFocusInWindow()
  }
)