Code Sketch


yoi fgg
By: Mhalsakant School
Category: Programming
import javax.swing._
import javax.swing.border.EmptyBorder
import java.awt._
import java.awt.event._
import java.io._
import java.nio.file.{Files, Paths}
import java.security.MessageDigest
import java.util.Base64
import scala.collection.mutable

val APP_DIR = new File(System.getProperty("user.home"), ".kojo_private_chat")
val DATA_FILE = new File(APP_DIR, "accounts.dat")

if (!APP_DIR.exists()) {
  APP_DIR.mkdirs()
}

case class User(
  name: String,
  passwordHash: String,
  friends: mutable.Set[String],
  incoming: mutable.Set[String],
  outgoing: mutable.Set[String]
) extends Serializable

case class ChatMessage(
  from: String,
  to: String,
  text: String,
  time: Long
) extends Serializable

class Database extends Serializable {
  val users: mutable.Map[String, User] = mutable.Map.empty
  val messages: mutable.ArrayBuffer[ChatMessage] = mutable.ArrayBuffer.empty
}

object ChatStore {
  private def hash(value: String): String = {
    val md = MessageDigest.getInstance("SHA-256")
    val bytes = md.digest(value.getBytes("UTF-8"))
    bytes.map("%02x".format(_)).mkString
  }

  def passwordHash(password: String): String = hash(password)

  def cleanName(s: String): String = {
    if (s == null) "" else s.trim.replaceAll("\\s+", " ")
  }

  def normalizeName(s: String): String = cleanName(s).toLowerCase

  def validPassword(p: String): Boolean = {
    p != null && p.length >= 4
  }

  def load(): Database = {
    if (!DATA_FILE.exists()) {
      return new Database
    }

    try {
      val in = new ObjectInputStream(new FileInputStream(DATA_FILE))
      val obj = in.readObject()
      in.close()

      obj match {
        case d: Database => d
        case _ => new Database
      }
    } catch {
      case _: Throwable => new Database
    }
  }

  def save(db: Database): Unit = {
    try {
      if (!APP_DIR.exists()) {
        APP_DIR.mkdirs()
      }

      val temp = new File(APP_DIR, "accounts.tmp")
      val out = new ObjectOutputStream(new FileOutputStream(temp))
      out.writeObject(db)
      out.flush()
      out.close()

      if (DATA_FILE.exists()) {
        DATA_FILE.delete()
      }

      temp.renameTo(DATA_FILE)
    } catch {
      case _: Throwable =>
    }
  }
}

val db = ChatStore.load()

def findUser(name: String): Option[User] = {
  val key = ChatStore.normalizeName(name)
  db.users.values.find(u => ChatStore.normalizeName(u.name) == key)
}

def uniqueUsername(name: String): Boolean = {
  findUser(name).isEmpty
}

def safeUser(name: String): Option[User] = findUser(name)

def displayName(name: String): String = {
  safeUser(name).map(_.name).getOrElse(name)
}

class RoundedPanel(bg: Color, radius: Int) extends JPanel {
  setOpaque(false)

  override def paintComponent(g: Graphics): Unit = {
    val g2 = g.create().asInstanceOf[Graphics2D]
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
    g2.setColor(bg)
    g2.fillRoundRect(0, 0, getWidth, getHeight, radius, radius)
    g2.dispose()
    super.paintComponent(g)
  }
}

class ChatPanel(currentUser: User, otherUser: User, onBack: () => Unit) extends JPanel {

  private val messagesArea = new JTextArea()
  private val input = new JTextField()
  private val sendButton = new JButton("SEND")
  private val backButton = new JButton("BACK")
  private val refreshButton = new JButton("REFRESH")

  setLayout(new BorderLayout(10, 10))
  setBackground(new Color(245, 247, 250))
  setBorder(new EmptyBorder(12, 12, 12, 12))

  val header = new JPanel(new BorderLayout())
  header.setBackground(new Color(24, 119, 242))
  header.setBorder(new EmptyBorder(10, 12, 10, 12))

  val title = new JLabel("  " + otherUser.name)
  title.setForeground(Color.WHITE)
  title.setFont(new Font("SansSerif", Font.BOLD, 20))

  backButton.setFocusPainted(false)
  refreshButton.setFocusPainted(false)

  header.add(backButton, BorderLayout.WEST)
  header.add(title, BorderLayout.CENTER)
  header.add(refreshButton, BorderLayout.EAST)

  add(header, BorderLayout.NORTH)

  messagesArea.setEditable(false)
  messagesArea.setLineWrap(true)
  messagesArea.setWrapStyleWord(true)
  messagesArea.setFont(new Font("SansSerif", Font.PLAIN, 15))
  messagesArea.setBackground(Color.WHITE)
  messagesArea.setBorder(new EmptyBorder(12, 12, 12, 12))

  val scroll = new JScrollPane(messagesArea)
  scroll.setBorder(BorderFactory.createLineBorder(new Color(220, 224, 230)))
  add(scroll, BorderLayout.CENTER)

  val bottom = new JPanel(new BorderLayout(8, 8))
  bottom.setBackground(new Color(245, 247, 250))

  input.setFont(new Font("SansSerif", Font.PLAIN, 15))
  input.setBorder(BorderFactory.createCompoundBorder(
    BorderFactory.createLineBorder(new Color(200, 205, 212)),
    new EmptyBorder(8, 8, 8, 8)
  ))

  sendButton.setBackground(new Color(24, 119, 242))
  sendButton.setForeground(Color.WHITE)
  sendButton.setFocusPainted(false)

  bottom.add(input, BorderLayout.CENTER)
  bottom.add(sendButton, BorderLayout.EAST)

  add(bottom, BorderLayout.SOUTH)

  def refreshMessages(): Unit = {
    messagesArea.setText("")

    val relevant = db.messages.filter { m =>
      (ChatStore.normalizeName(m.from) == ChatStore.normalizeName(currentUser.name) &&
        ChatStore.normalizeName(m.to) == ChatStore.normalizeName(otherUser.name)) ||
      (ChatStore.normalizeName(m.from) == ChatStore.normalizeName(otherUser.name) &&
        ChatStore.normalizeName(m.to) == ChatStore.normalizeName(currentUser.name))
    }

    relevant.foreach { m =>
      val date = new java.util.Date(m.time)
      messagesArea.append(
        m.from + "  [" + date.toString + "]\n" +
        m.text + "\n\n"
      )
    }

    messagesArea.setCaretPosition(messagesArea.getDocument.getLength)
  }

  def sendMessage(): Unit = {
    val text = input.getText.trim

    if (text.nonEmpty) {
      val stillFriends =
        currentUser.friends.exists(x =>
          ChatStore.normalizeName(x) == ChatStore.normalizeName(otherUser.name)
        )

      if (stillFriends) {
        db.messages += ChatMessage(
          currentUser.name,
          otherUser.name,
          text,
          System.currentTimeMillis()
        )

        ChatStore.save(db)
        input.setText("")
        refreshMessages()
      } else {
        JOptionPane.showMessageDialog(
          this,
          "You are no longer connected with this person.",
          "Chat unavailable",
          JOptionPane.WARNING_MESSAGE
        )
        onBack()
      }
    }
  }

  sendButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = sendMessage()
  })

  input.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = sendMessage()
  })

  refreshButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = refreshMessages()
  })

  backButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = onBack()
  })

  refreshMessages()
}

class MainPanel(currentUser: User, frame: JFrame) extends JPanel {

  private var selectedFriend: Option[User] = None

  private val title = new JLabel("Private Chat")
  private val userLabel = new JLabel()
  private val searchField = new JTextField()
  private val searchButton = new JButton("SEARCH PEOPLE")
  private val resultsPanel = new JPanel()
  private val friendsPanel = new JPanel()
  private val incomingPanel = new JPanel()

  setLayout(new BorderLayout())
  setBackground(new Color(245, 247, 250))

  val top = new JPanel(new BorderLayout())
  top.setBackground(new Color(24, 119, 242))
  top.setBorder(new EmptyBorder(12, 16, 12, 16))

  title.setForeground(Color.WHITE)
  title.setFont(new Font("SansSerif", Font.BOLD, 24))

  userLabel.setForeground(Color.WHITE)
  userLabel.setFont(new Font("SansSerif", Font.PLAIN, 14))
  userLabel.setHorizontalAlignment(SwingConstants.RIGHT)

  top.add(title, BorderLayout.WEST)
  top.add(userLabel, BorderLayout.EAST)

  add(top, BorderLayout.NORTH)

  val center = new JPanel()
  center.setLayout(new BoxLayout(center, BoxLayout.Y_AXIS))
  center.setBackground(new Color(245, 247, 250))
  center.setBorder(new EmptyBorder(14, 14, 14, 14))

  val searchCard = new JPanel(new BorderLayout(8, 8))
  searchCard.setBackground(Color.WHITE)
  searchCard.setBorder(new EmptyBorder(12, 12, 12, 12))

  searchField.setFont(new Font("SansSerif", Font.PLAIN, 15))
  searchField.setBorder(BorderFactory.createLineBorder(new Color(210, 214, 220)))

  searchButton.setBackground(new Color(24, 119, 242))
  searchButton.setForeground(Color.WHITE)
  searchButton.setFocusPainted(false)

  searchCard.add(searchField, BorderLayout.CENTER)
  searchCard.add(searchButton, BorderLayout.EAST)

  center.add(searchCard)
  center.add(Box.createVerticalStrut(10))

  val resultTitle = new JLabel("SEARCH RESULTS")
  resultTitle.setFont(new Font("SansSerif", Font.BOLD, 13))
  resultTitle.setForeground(new Color(90, 95, 105))

  center.add(resultTitle)
  center.add(Box.createVerticalStrut(5))

  resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.Y_AXIS))
  resultsPanel.setBackground(new Color(245, 247, 250))

  val resultScroll = new JScrollPane(resultsPanel)
  resultScroll.setPreferredSize(new Dimension(600, 130))
  resultScroll.setBorder(BorderFactory.createLineBorder(new Color(225, 228, 233)))

  center.add(resultScroll)
  center.add(Box.createVerticalStrut(12))

  val incomingTitle = new JLabel("FRIEND REQUESTS")
  incomingTitle.setFont(new Font("SansSerif", Font.BOLD, 13))
  incomingTitle.setForeground(new Color(90, 95, 105))

  center.add(incomingTitle)
  center.add(Box.createVerticalStrut(5))

  incomingPanel.setLayout(new BoxLayout(incomingPanel, BoxLayout.Y_AXIS))
  incomingPanel.setBackground(new Color(245, 247, 250))

  val incomingScroll = new JScrollPane(incomingPanel)
  incomingScroll.setPreferredSize(new Dimension(600, 130))
  incomingScroll.setBorder(BorderFactory.createLineBorder(new Color(225, 228, 233)))

  center.add(incomingScroll)
  center.add(Box.createVerticalStrut(12))

  val friendsTitle = new JLabel("YOUR FRIENDS")
  friendsTitle.setFont(new Font("SansSerif", Font.BOLD, 13))
  friendsTitle.setForeground(new Color(90, 95, 105))

  center.add(friendsTitle)
  center.add(Box.createVerticalStrut(5))

  friendsPanel.setLayout(new BoxLayout(friendsPanel, BoxLayout.Y_AXIS))
  friendsPanel.setBackground(new Color(245, 247, 250))

  val friendsScroll = new JScrollPane(friendsPanel)
  friendsScroll.setPreferredSize(new Dimension(600, 170))
  friendsScroll.setBorder(BorderFactory.createLineBorder(new Color(225, 228, 233)))

  center.add(friendsScroll)

  add(center, BorderLayout.CENTER)

  val bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT))
  bottom.setBackground(new Color(245, 247, 250))

  val profileButton = new JButton("PROFILE / PASSWORD")
  val logoutButton = new JButton("LOG OUT")

  profileButton.setFocusPainted(false)
  logoutButton.setFocusPainted(false)

  bottom.add(profileButton)
  bottom.add(logoutButton)

  add(bottom, BorderLayout.SOUTH)

  def makeUserRow(user: User): JPanel = {
    val row = new JPanel(new BorderLayout(8, 8))
    row.setBackground(Color.WHITE)
    row.setBorder(new EmptyBorder(8, 10, 8, 10))
    row.setMaximumSize(new Dimension(1000, 52))

    val name = new JLabel(user.name)
    name.setFont(new Font("SansSerif", Font.BOLD, 15))

    row.add(name, BorderLayout.CENTER)
    row
  }

  def makeSearchRow(user: User): JPanel = {
    val row = new JPanel(new BorderLayout(8, 8))
    row.setBackground(Color.WHITE)
    row.setBorder(new EmptyBorder(8, 10, 8, 10))
    row.setMaximumSize(new Dimension(1000, 52))

    val name = new JLabel(user.name)
    name.setFont(new Font("SansSerif", Font.BOLD, 15))

    val action = new JButton()

    val isFriend = currentUser.friends.exists(
      x => ChatStore.normalizeName(x) == ChatStore.normalizeName(user.name)
    )

    val alreadySent = currentUser.outgoing.exists(
      x => ChatStore.normalizeName(x) == ChatStore.normalizeName(user.name)
    )

    val incoming = currentUser.incoming.exists(
      x => ChatStore.normalizeName(x) == ChatStore.normalizeName(user.name)
    )

    if (isFriend) {
      action.setText("CHAT")
    } else if (incoming) {
      action.setText("REQUEST RECEIVED")
      action.setEnabled(false)
    } else if (alreadySent) {
      action.setText("REQUEST SENT")
      action.setEnabled(false)
    } else {
      action.setText("ADD FRIEND")
    }

    action.setFocusPainted(false)

    action.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        if (isFriend) {
          openChat(user)
        } else {
          sendRequest(user)
        }
      }
    })

    row.add(name, BorderLayout.CENTER)
    row.add(action, BorderLayout.EAST)

    row
  }

  def sendRequest(target: User): Unit = {
    val exists =
      currentUser.outgoing.exists(
        x => ChatStore.normalizeName(x) == ChatStore.normalizeName(target.name)
      )

    if (!exists) {
      currentUser.outgoing += target.name
      target.incoming += currentUser.name
      ChatStore.save(db)

      JOptionPane.showMessageDialog(
        frame,
        "Friend request sent to " + target.name,
        "Friend request",
        JOptionPane.INFORMATION_MESSAGE
      )

      refreshAll()
    }
  }

  def acceptRequest(from: User): Unit = {
    currentUser.incoming -= from.name
    from.outgoing -= currentUser.name

    currentUser.friends += from.name
    from.friends += currentUser.name

    ChatStore.save(db)

    JOptionPane.showMessageDialog(
      frame,
      "You and " + from.name + " are now friends.",
      "Friend added",
      JOptionPane.INFORMATION_MESSAGE
    )

    refreshAll()
  }

  def rejectRequest(from: User): Unit = {
    currentUser.incoming -= from.name
    from.outgoing -= currentUser.name
    ChatStore.save(db)
    refreshAll()
  }

  def openChat(friend: User): Unit = {
    selectedFriend = Some(friend)

    val chat = new ChatPanel(
      currentUser,
      friend,
      () => {
        frame.setContentPane(this)
        frame.revalidate()
        frame.repaint()
        refreshAll()
      }
    )

    frame.setContentPane(chat)
    frame.revalidate()
    frame.repaint()
    chat.requestFocusInWindow()
  }

  def searchPeople(): Unit = {
    resultsPanel.removeAll()

    val query = ChatStore.normalizeName(searchField.getText)

    if (query.isEmpty) {
      resultsPanel.add(new JLabel("Type a name to search."))
    } else {
      val found = db.users.values
        .filter(u => ChatStore.normalizeName(u.name).contains(query))
        .filter(u => ChatStore.normalizeName(u.name) != ChatStore.normalizeName(currentUser.name))
        .toSeq
        .sortBy(_.name.toLowerCase)

      if (found.isEmpty) {
        resultsPanel.add(new JLabel("No user found."))
      } else {
        found.foreach { u =>
          resultsPanel.add(makeSearchRow(u))
          resultsPanel.add(Box.createVerticalStrut(4))
        }
      }
    }

    resultsPanel.revalidate()
    resultsPanel.repaint()
  }

  def refreshIncoming(): Unit = {
    incomingPanel.removeAll()

    val names = currentUser.incoming.toSeq.sorted

    if (names.isEmpty) {
      incomingPanel.add(new JLabel("No pending friend requests."))
    } else {
      names.foreach { name =>
        safeUser(name) match {
          case Some(user) =>
            val row = new JPanel(new BorderLayout(8, 8))
            row.setBackground(Color.WHITE)
            row.setBorder(new EmptyBorder(8, 10, 8, 10))
            row.setMaximumSize(new Dimension(1000, 55))

            val label = new JLabel(user.name)
            label.setFont(new Font("SansSerif", Font.BOLD, 15))

            val buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0))
            buttons.setOpaque(false)

            val accept = new JButton("ACCEPT")
            val reject = new JButton("DECLINE")

            accept.setFocusPainted(false)
            reject.setFocusPainted(false)

            accept.addActionListener(new ActionListener {
              override def actionPerformed(e: ActionEvent): Unit = {
                acceptRequest(user)
              }
            })

            reject.addActionListener(new ActionListener {
              override def actionPerformed(e: ActionEvent): Unit = {
                rejectRequest(user)
              }
            })

            buttons.add(accept)
            buttons.add(reject)

            row.add(label, BorderLayout.CENTER)
            row.add(buttons, BorderLayout.EAST)

            incomingPanel.add(row)
            incomingPanel.add(Box.createVerticalStrut(4))

          case None =>
            currentUser.incoming -= name
        }
      }
    }

    incomingPanel.revalidate()
    incomingPanel.repaint()
  }

  def refreshFriends(): Unit = {
    friendsPanel.removeAll()

    val names = currentUser.friends.toSeq.sorted

    if (names.isEmpty) {
      friendsPanel.add(new JLabel("No friends yet. Search people above and send a request."))
    } else {
      names.foreach { name =>
        safeUser(name) match {
          case Some(user) =>
            val row = new JPanel(new BorderLayout(8, 8))
            row.setBackground(Color.WHITE)
            row.setBorder(new EmptyBorder(8, 10, 8, 10))
            row.setMaximumSize(new Dimension(1000, 55))

            val label = new JLabel(user.name)
            label.setFont(new Font("SansSerif", Font.BOLD, 15))

            val chat = new JButton("OPEN PRIVATE CHAT")
            chat.setBackground(new Color(24, 119, 242))
            chat.setForeground(Color.WHITE)
            chat.setFocusPainted(false)

            chat.addActionListener(new ActionListener {
              override def actionPerformed(e: ActionEvent): Unit = {
                openChat(user)
              }
            })

            row.add(label, BorderLayout.CENTER)
            row.add(chat, BorderLayout.EAST)

            friendsPanel.add(row)
            friendsPanel.add(Box.createVerticalStrut(4))

          case None =>
            currentUser.friends -= name
        }
      }
    }

    friendsPanel.revalidate()
    friendsPanel.repaint()
  }

  def refreshAll(): Unit = {
    userLabel.setText("Signed in: " + currentUser.name)
    refreshIncoming()
    refreshFriends()
    searchPeople()
  }

  profileButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      val newPassword = JOptionPane.showInputDialog(
        frame,
        "Enter new password:",
        "Change Password",
        JOptionPane.PLAIN_MESSAGE
      )

      if (newPassword != null) {
        if (ChatStore.validPassword(newPassword)) {
          currentUser.copy(passwordHash = ChatStore.passwordHash(newPassword))
          val replacement = currentUser.copy(
            passwordHash = ChatStore.passwordHash(newPassword),
            friends = currentUser.friends,
            incoming = currentUser.incoming,
            outgoing = currentUser.outgoing
          )

          db.users.update(ChatStore.normalizeName(currentUser.name), replacement)
          ChatStore.save(db)

          JOptionPane.showMessageDialog(
            frame,
            "Password changed successfully.",
            "Profile",
            JOptionPane.INFORMATION_MESSAGE
          )
        } else {
          JOptionPane.showMessageDialog(
            frame,
            "Password must contain at least 4 characters.",
            "Invalid password",
            JOptionPane.WARNING_MESSAGE
          )
        }
      }
    }
  })

  logoutButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      showLogin(frame)
    }
  })

  searchButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      searchPeople()
    }
  })

  searchField.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = {
      searchPeople()
    }
  })

  refreshAll()
}

def showLogin(frame: JFrame): Unit = {

  val panel = new JPanel(new GridBagLayout())
  panel.setBackground(new Color(245, 247, 250))

  val card = new JPanel(new GridBagLayout())
  card.setBackground(Color.WHITE)
  card.setBorder(new EmptyBorder(28, 34, 28, 34))

  val c = new GridBagConstraints()
  c.insets = new Insets(7, 7, 7, 7)
  c.fill = GridBagConstraints.HORIZONTAL

  val logo = new JLabel("PRIVATE CHAT")
  logo.setFont(new Font("SansSerif", Font.BOLD, 28))
  logo.setForeground(new Color(24, 119, 242))

  val subtitle = new JLabel("Private friend-to-friend messaging")
  subtitle.setForeground(new Color(90, 95, 105))

  val nameField = new JTextField()
  val passwordField = new JPasswordField()

  nameField.setPreferredSize(new Dimension(300, 38))
  passwordField.setPreferredSize(new Dimension(300, 38))

  val loginButton = new JButton("SIGN IN")
  val registerButton = new JButton("CREATE ACCOUNT")

  loginButton.setBackground(new Color(24, 119, 242))
  loginButton.setForeground(Color.WHITE)
  loginButton.setFocusPainted(false)

  registerButton.setFocusPainted(false)

  c.gridx = 0
  c.gridy = 0
  card.add(logo, c)

  c.gridy = 1
  card.add(subtitle, c)

  c.gridy = 2
  card.add(new JLabel("Name"), c)

  c.gridy = 3
  card.add(nameField, c)

  c.gridy = 4
  card.add(new JLabel("Password"), c)

  c.gridy = 5
  card.add(passwordField, c)

  c.gridy = 6
  card.add(loginButton, c)

  c.gridy = 7
  card.add(registerButton, c)

  panel.add(card)

  frame.setContentPane(panel)
  frame.revalidate()
  frame.repaint()

  def signIn(): Unit = {
    val name = ChatStore.cleanName(nameField.getText)
    val password = new String(passwordField.getPassword)

    if (name.isEmpty || password.isEmpty) {
      JOptionPane.showMessageDialog(
        frame,
        "Please enter name and password.",
        "Sign in",
        JOptionPane.WARNING_MESSAGE
      )
      return
    }

    safeUser(name) match {
      case Some(user) =>
        if (user.passwordHash == ChatStore.passwordHash(password)) {
          val main = new MainPanel(user, frame)
          frame.setContentPane(main)
          frame.revalidate()
          frame.repaint()
        } else {
          JOptionPane.showMessageDialog(
            frame,
            "Incorrect password.",
            "Sign in failed",
            JOptionPane.ERROR_MESSAGE
          )
        }

      case None =>
        JOptionPane.showMessageDialog(
          frame,
          "Account not found. Create an account first.",
          "Sign in failed",
          JOptionPane.ERROR_MESSAGE
        )
    }
  }

  def createAccount(): Unit = {
    val name = ChatStore.cleanName(nameField.getText)
    val password = new String(passwordField.getPassword)

    if (name.length < 2) {
      JOptionPane.showMessageDialog(
        frame,
        "Name must contain at least 2 characters.",
        "Create account",
        JOptionPane.WARNING_MESSAGE
      )
      return
    }

    if (!ChatStore.validPassword(password)) {
      JOptionPane.showMessageDialog(
        frame,
        "Password must contain at least 4 characters.",
        "Create account",
        JOptionPane.WARNING_MESSAGE
      )
      return
    }

    if (!uniqueUsername(name)) {
      JOptionPane.showMessageDialog(
        frame,
        "That name is already registered. Please choose another name.",
        "Create account",
        JOptionPane.WARNING_MESSAGE
      )
      return
    }

    val user = User(
      name,
      ChatStore.passwordHash(password),
      mutable.Set.empty[String],
      mutable.Set.empty[String],
      mutable.Set.empty[String]
    )

    db.users += (ChatStore.normalizeName(name) -> user)
    ChatStore.save(db)

    JOptionPane.showMessageDialog(
      frame,
      "Account created successfully. You can now sign in.",
      "Account created",
      JOptionPane.INFORMATION_MESSAGE
    )

    nameField.setText(name)
    passwordField.setText("")
    passwordField.requestFocusInWindow()
  }

  loginButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = signIn()
  })

  registerButton.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = createAccount()
  })

  passwordField.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = signIn()
  })

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

val frame = new JFrame("Private Chat")
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setMinimumSize(new Dimension(760, 620))
frame.setContentPane(new JPanel())
frame.setSize(820, 680)
frame.setLocationRelativeTo(null)

showLogin(frame)

frame.setVisible(true)

SwingUtilities.invokeLater(new Runnable {
  override def run(): Unit = {
    frame.requestFocus()
  }
})