Code Sketch


yoi chat
By: Mhalsakant School
Category: Programming
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
import java.awt.GridLayout
import java.awt.Image
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.image.BufferedImage
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent

import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter

import java.net.Inet4Address
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket
import java.net.DatagramPacket
import java.net.DatagramSocket

import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

import java.security.MessageDigest
import java.security.SecureRandom

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

import java.util.Base64
import java.util.Properties
import java.util.UUID

import javax.imageio.ImageIO

import javax.swing.BorderFactory
import javax.swing.DefaultListModel
import javax.swing.JDialog
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.JOptionPane
import javax.swing.JPanel
import javax.swing.JPasswordField
import javax.swing.JScrollPane
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.JButton
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.UIManager
import javax.swing.WindowConstants

import scala.collection.mutable


object LiveChatApp {

  // ==========================================================
  // SETTINGS
  // ==========================================================

  private val APP_NAME =
    "YADNESH LIVE CHAT"

  private val DEFAULT_PORT =
    5050


  // ==========================================================
  // UTILITY
  // ==========================================================

  object Util {

    private val random =
      new SecureRandom()

    private val encoder =
      Base64.getUrlEncoder.withoutPadding()

    private val decoder =
      Base64.getUrlDecoder


    def encode(
        value: String
    ): String = {

      val text =
        if (value == null) "" else value

      encoder.encodeToString(
        text.getBytes(
          StandardCharsets.UTF_8
        )
      )
    }


    def decode(
        value: String
    ): String = {

      if (
        value == null ||
        value.isEmpty
      ) {
        ""
      } else {

        try {

          new String(
            decoder.decode(value),
            StandardCharsets.UTF_8
          )

        } catch {

          case _: Throwable =>
            ""
        }
      }
    }


    def sha256(
        value: String
    ): String = {

      val md =
        MessageDigest.getInstance(
          "SHA-256"
        )

      md.digest(
        value.getBytes(
          StandardCharsets.UTF_8
        )
      ).map(
        "%02x".format(_)
      ).mkString
    }


    def randomHex(
        size: Int
    ): String = {

      val bytes =
        new Array[Byte](size)

      random.nextBytes(
        bytes
      )

      bytes.map(
        "%02x".format(_)
      ).mkString
    }


    def secureEquals(
        a: String,
        b: String
    ): Boolean = {

      if (
        a == null ||
        b == null
      ) {
        false
      } else {

        MessageDigest.isEqual(
          a.getBytes(
            StandardCharsets.UTF_8
          ),
          b.getBytes(
            StandardCharsets.UTF_8
          )
        )
      }
    }


    def clean(
        value: String,
        maximum: Int
    ): String = {

      val text =
        if (value == null) "" else value.trim

      if (
        text.length <= maximum
      ) {
        text
      } else {
        text.substring(
          0,
          maximum
        )
      }
    }


    def validUsername(
        name: String
    ): Boolean = {

      name != null &&
      name.matches(
        "[A-Za-z0-9_]{3,20}"
      )
    }


    def now(): String = {

      LocalDateTime.now().format(
        DateTimeFormatter.ofPattern(
          "HH:mm:ss"
        )
      )
    }


    // --------------------------------------------------------
    // IMAGE -> BASE64
    // --------------------------------------------------------

    def imageToBase64(
        original: Image
    ): String = {

      if (original == null) {
        return ""
      }

      try {

        val width =
          math.max(
            1,
            original.getWidth(null)
          )

        val height =
          math.max(
            1,
            original.getHeight(null)
          )

        val image =
          new BufferedImage(
            width,
            height,
            BufferedImage.TYPE_INT_ARGB
          )

        val graphics =
          image.createGraphics()

        try {

          graphics.drawImage(
            original,
            0,
            0,
            null
          )

        } finally {

          graphics.dispose()
        }

        val out =
          new ByteArrayOutputStream()

        ImageIO.write(
          image,
          "png",
          out
        )

        out.close()

        val data =
          out.toByteArray

        if (
          data.length >
            350 * 1024
        ) {

          ""

        } else {

          encoder.encodeToString(
            data
          )
        }

      } catch {

        case _: Throwable =>
          ""
      }
    }


    // --------------------------------------------------------
    // FILE -> BASE64
    // --------------------------------------------------------

    def fileToBase64(
        path: String
    ): String = {

      try {

        val image =
          ImageIO.read(
            new java.io.File(
              path
            )
          )

        imageToBase64(
          image
        )

      } catch {

        case _: Throwable =>
          ""
      }
    }


    // --------------------------------------------------------
    // BASE64 -> IMAGE
    // --------------------------------------------------------

    def base64ToImage(
        value: String
    ): BufferedImage = {

      if (
        value == null ||
        value.isEmpty
      ) {

        null

      } else {

        try {

          val bytes =
            decoder.decode(
              value
            )

          ImageIO.read(
            new ByteArrayInputStream(
              bytes
            )
          )

        } catch {

          case _: Throwable =>
            null
        }
      }
    }


    // --------------------------------------------------------
    // PASTE IMAGE FROM CLIPBOARD
    // --------------------------------------------------------

    def clipboardImageBase64():
        String = {

      try {

        val clipboard =
          Toolkit
            .getDefaultToolkit
            .getSystemClipboard

        if (
          clipboard.isDataFlavorAvailable(
            DataFlavor.imageFlavor
          )
        ) {

          val image =
            clipboard.getData(
              DataFlavor.imageFlavor
            ).asInstanceOf[Image]

          imageToBase64(
            image
          )

        } else {

          ""
        }

      } catch {

        case _: Throwable =>
          ""
      }
    }


    // --------------------------------------------------------
    // LAN IPv4
    // --------------------------------------------------------

    def localIPs():
        Seq[String] = {

      val values =
        mutable.ListBuffer[String]()

      try {

        val interfaces =
          NetworkInterface.getNetworkInterfaces()

        while (
          interfaces != null &&
          interfaces.hasMoreElements
        ) {

          val network =
            interfaces.nextElement()

          if (
            network.isUp &&
            !network.isLoopback &&
            !network.isVirtual
          ) {

            val addresses =
              network.getInetAddresses()

            while (
              addresses.hasMoreElements
            ) {

              val address =
                addresses.nextElement()

              if (
                address
                  .isInstanceOf[Inet4Address] &&
                !address.isLoopbackAddress
              ) {

                val ip =
                  address.getHostAddress()

                if (
                  !values.contains(
                    ip
                  )
                ) {

                  values +=
                    ip
                }
              }
            }
          }
        }

      } catch {

        case _: Throwable =>
      }

      if (
        values.isEmpty
      ) {

        Seq(
          "127.0.0.1"
        )

      } else {

        values.toSeq
      }
    }


    def setupFont(): Unit = {

      val font =
        new Font(
          "Segoe UI",
          Font.PLAIN,
          14
        )

      UIManager.put(
        "Label.font",
        font
      )

      UIManager.put(
        "Button.font",
        font
      )

      UIManager.put(
        "TextField.font",
        font
      )

      UIManager.put(
        "PasswordField.font",
        font
      )

      UIManager.put(
        "TextArea.font",
        font
      )

      UIManager.put(
        "List.font",
        font
      )
    }
  }


  // ==========================================================
  // DATA
  // ==========================================================

  case class UserRecord(
      username: String,
      passwordSalt: String,
      passwordVerifier: String,
      bio: String,
      avatar: String,
      friends: Set[String] = Set.empty[String],
      incomingRequests: Set[String] = Set.empty[String],
      blocked: Set[String] = Set.empty[String]
  )


  case class ChatMessage(
      sender: String,
      text: String,
      time: String
  )


  case class PersonItem(
      username: String,
      bio: String,
      online: Boolean,
      relation: String = "NONE"
  ) {

    override def toString: String = {

      val state =
        if (online) "? ONLINE" else "? OFFLINE"

      val rel =
        relation match {
          case "FRIEND"   => "   ? FRIEND"
          case "INCOMING" => "   ? REQUEST"
          case "OUTGOING" => "   ? REQUEST SENT"
          case "BLOCKED"  => "   ? BLOCKED"
          case _          => ""
        }

      username +
        "   " +
        state +
        rel
    }
  }


  case class ChatItem(
      id: String,
      title: String,
      group: Boolean,
      unread: Int = 0
  ) {

    override def toString: String = {
      if (unread > 0) {
        title + "   (" + unread + " new)"
      } else {
        title
      }
    }
  }



  // ==========================================================
  // AUTOMATIC LAN DISCOVERY
  // ==========================================================
  object LanDiscovery {

    private val DISCOVERY_PORT =
      5051

    private val MAGIC =
      "YADNESH_LIVE_CHAT_SERVER|"

    @volatile private var beaconRunning =
      false

    private var beaconThread:
        Thread = null

    def startBeacon(
        tcpPort: Int
    ): Unit = {

      if (beaconRunning) {
        return
      }

      beaconRunning = true

      beaconThread =
        new Thread(
          new Runnable {

            override def run(): Unit = {

              var socket:
                  DatagramSocket = null

              try {

                socket =
                  new DatagramSocket()

                socket.setBroadcast(
                  true
                )

                while (
                  beaconRunning
                ) {

                  val bytes =
                    (
                      MAGIC +
                        tcpPort
                    ).getBytes(
                      StandardCharsets.UTF_8
                    )

                  val packet =
                    new DatagramPacket(
                      bytes,
                      bytes.length,
                      java.net.InetAddress
                        .getByName(
                          "255.255.255.255"
                        ),
                      DISCOVERY_PORT
                    )

                  try {
                    socket.send(packet)
                  } catch {
                    case _: Throwable =>
                  }

                  try {
                    Thread.sleep(
                      1000L
                    )
                  } catch {
                    case _: InterruptedException =>
                      beaconRunning = false
                  }
                }

              } catch {
                case _: Throwable =>
              } finally {

                if (
                  socket != null
                ) {
                  try {
                    socket.close()
                  } catch {
                    case _: Throwable =>
                  }
                }
              }
            }
          },
          "Yadnesh-Chat-Beacon"
        )

      beaconThread.setDaemon(
        true
      )

      beaconThread.start()
    }

    def stopBeacon(): Unit = {

      beaconRunning = false

      if (
        beaconThread != null
      ) {
        try {
          beaconThread.interrupt()
        } catch {
          case _: Throwable =>
        }
      }

      beaconThread = null
    }

    def findServer(
        timeoutMs: Int
    ): Option[(String, Int)] = {

      var socket:
          DatagramSocket = null

      try {

        socket =
          new DatagramSocket(
            DISCOVERY_PORT
          )

        socket.setSoTimeout(
          math.max(
            100,
            timeoutMs
          )
        )

        val buffer =
          new Array[Byte](
            512
          )

        val packet =
          new DatagramPacket(
            buffer,
            buffer.length
          )

        socket.receive(
          packet
        )

        val text =
          new String(
            packet.getData,
            packet.getOffset,
            packet.getLength,
            StandardCharsets.UTF_8
          ).trim

        if (
          text.startsWith(
            MAGIC
          )
        ) {

          val portText =
            text.substring(
              MAGIC.length
            )

          try {

            val port =
              portText.toInt

            if (
              port >= 1024 &&
              port <= 65535
            ) {
              Some(
                (
                  packet
                    .getAddress
                    .getHostAddress,
                  port
                )
              )
            } else {
              None
            }

          } catch {
            case _: Throwable =>
              None
          }

        } else {
          None
        }

      } catch {
        case _: java.net.SocketTimeoutException =>
          None

        case _: Throwable =>
          None

      } finally {

        if (
          socket != null
        ) {
          try {
            socket.close()
          } catch {
            case _: Throwable =>
          }
        }
      }
    }
  }


  // ==========================================================
  // SERVER
  // ==========================================================

  object Server {

    private val lock =
      new Object


    private val users =
      mutable.Map[
        String,
        UserRecord
      ]()


    private val sessions =
      mutable.Map[
        String,
        ClientConnection
      ]()


    private val chatMembers =
      mutable.Map[
        String,
        mutable.Set[String]
      ]()


    private val chatNames =
      mutable.Map[
        String,
        String
      ]()


    private val chatGroups =
      mutable.Map[
        String,
        Boolean
      ]()


    private val chatHistory =
      mutable.Map[
        String,
        mutable.ListBuffer[
          ChatMessage
        ]
      ]()


    private val loginChallenges =
      mutable.Map[
        Socket,
        (String, String)
      ]()


    private val registerChallenges =
      mutable.Map[
        Socket,
        (String, String)
      ]()


    private val recoveryChallenges =
      mutable.Map[
        Socket,
        (String, String)
      ]()


    // --------------------------------------------------------
    // RECOVERY SECRET HASH
    // --------------------------------------------------------
    //
    // This is the SHA-256 verifier of the private recovery
    // secret. The secret itself is not shown in the UI/code.
    //
    // --------------------------------------------------------

    private val recoveryVerifier =
      "7f3f667469f4945d7b63b6831e26f1bd84bd02d57b455542b6144fe2b2a6a92d"


    private val dataFolder =
      Paths.get(
        "YadneshLiveChatData"
      )


    private val profilesFolder =
      dataFolder.resolve(
        "profiles"
      )


    private var serverSocket:
        ServerSocket = null


    private var running =
      false


    // ========================================================
    // CONNECTION
    // ========================================================

    class ClientConnection(
        val socket: Socket,
        val reader: BufferedReader,
        val writer: PrintWriter
    ) {

      var username =
        ""


      def send(
          line: String
      ): Unit = {

        lock.synchronized {

          try {

            if (
              !socket.isClosed
            ) {

              writer.println(
                line
              )

              writer.flush()
            }

          } catch {

            case _: Throwable =>
          }
        }
      }


      def close(): Unit = {

        try {

          socket.close()

        } catch {

          case _: Throwable =>
        }
      }
    }


    // ========================================================
    // START SERVER
    // ========================================================

    def start(
        port: Int
    ): Boolean = {

      lock.synchronized {

        if (running) {

          true

        } else {

          try {

            Files.createDirectories(
              profilesFolder
            )

            loadProfiles()

            serverSocket =
              new ServerSocket()

            serverSocket.setReuseAddress(
              true
            )

            // Important:
            // listen on ALL network interfaces.
            serverSocket.bind(
              new InetSocketAddress(
                "0.0.0.0",
                port
              )
            )

            running =
              true


            val thread =
              new Thread(
                new Runnable {

                  override def run(): Unit = {

                    acceptLoop()
                  }
                }
              )


            thread.setDaemon(
              true
            )

            thread.start()

            LanDiscovery.startBeacon(
              port
            )

            true

          } catch {

            case _: Throwable =>

              running =
                false

              false
          }
        }
      }
    }


    // ========================================================
    // ACCEPT LOOP
    // ========================================================

    private def acceptLoop():
        Unit = {

      while (
        running
      ) {

        try {

          val sock =
            serverSocket.accept()

          sock.setKeepAlive(
            true
          )

          sock.setTcpNoDelay(
            true
          )


          val reader =
            new BufferedReader(
              new InputStreamReader(
                sock.getInputStream,
                StandardCharsets.UTF_8
              )
            )


          val writer =
            new PrintWriter(
              new OutputStreamWriter(
                sock.getOutputStream,
                StandardCharsets.UTF_8
              ),
              true
            )


          val client =
            new ClientConnection(
              sock,
              reader,
              writer
            )


          val thread =
            new Thread(
              new Runnable {

                override def run(): Unit = {

                  clientLoop(
                    client
                  )
                }
              }
            )


          thread.setDaemon(
            true
          )

          thread.start()

        } catch {

          case _: Throwable =>
        }
      }
    }


    // ========================================================
    // PROFILE FILE
    // ========================================================

    private def profileFile(
        username: String
    ): Path = {

      profilesFolder.resolve(
        username +
          ".profile"
      )
    }


    private def saveProfile(
        user: UserRecord
    ): Unit = {

      val props =
        new Properties()


      props.setProperty(
        "username",
        user.username
      )


      props.setProperty(
        "passwordSalt",
        user.passwordSalt
      )


      props.setProperty(
        "passwordVerifier",
        user.passwordVerifier
      )


      props.setProperty(
        "bio",
        user.bio
      )


      props.setProperty(
        "avatar",
        user.avatar
      )

      props.setProperty(
        "friends",
        user.friends.toSeq.sorted.mkString(",")
      )

      props.setProperty(
        "incomingRequests",
        user.incomingRequests.toSeq.sorted.mkString(",")
      )

      props.setProperty(
        "blocked",
        user.blocked.toSeq.sorted.mkString(",")
      )


      val out =
        Files.newOutputStream(
          profileFile(
            user.username
          )
        )


      try {

        props.store(
          out,
          "Yadnesh Live Chat Account"
        )

      } finally {

        out.close()
      }
    }


    // ========================================================
    // LOAD ALL ACCOUNTS
    // ========================================================

    private def loadProfiles():
        Unit = {

      users.clear()


      if (
        !Files.exists(
          profilesFolder
        )
      ) {

        return
      }


      val stream =
        Files.list(
          profilesFolder
        )


      try {

        val iterator =
          stream.iterator()


        while (
          iterator.hasNext
        ) {

          val path =
            iterator.next()


          if (
            path.toString
              .endsWith(
                ".profile"
              )
          ) {

            try {

              val props =
                new Properties()


              val input =
                Files.newInputStream(
                  path
                )


              try {

                props.load(
                  input
                )

              } finally {

                input.close()
              }


              val username =
                props.getProperty(
                  "username",
                  ""
                )


              if (
                Util.validUsername(
                  username
                )
              ) {

                val friends =
                  props
                    .getProperty(
                      "friends",
                      ""
                    )
                    .split(",")
                    .map(_.trim)
                    .filter(_.nonEmpty)
                    .toSet

                val incomingRequests =
                  props
                    .getProperty(
                      "incomingRequests",
                      ""
                    )
                    .split(",")
                    .map(_.trim)
                    .filter(_.nonEmpty)
                    .toSet

                val blocked =
                  props
                    .getProperty(
                      "blocked",
                      ""
                    )
                    .split(",")
                    .map(_.trim)
                    .filter(_.nonEmpty)
                    .toSet

                val user =
                  UserRecord(

                    username,

                    props.getProperty(
                      "passwordSalt",
                      ""
                    ),

                    props.getProperty(
                      "passwordVerifier",
                      ""
                    ),

                    props.getProperty(
                      "bio",
                      ""
                    ),

                    props.getProperty(
                      "avatar",
                      ""
                    ),

                    friends,
                    incomingRequests,
                    blocked
                  )


                users.put(
                  username.toLowerCase,
                  user
                )
              }

            } catch {

              case _: Throwable =>
            }
          }
        }

      } finally {

        stream.close()
      }
    }


    // ========================================================
    // FRIENDSHIP HELPERS
    // ========================================================

    private def canonicalUser(
        username: String
    ): Option[String] = {

      users.get(
        username.toLowerCase
      ).map(_.username)
    }


    private def isFriend(
        a: String,
        b: String
    ): Boolean = {

      users
        .get(a.toLowerCase)
        .exists(_.friends.exists(_.equalsIgnoreCase(b)))
    }


    private def isBlocked(
        a: String,
        b: String
    ): Boolean = {

      users
        .get(a.toLowerCase)
        .exists(_.blocked.exists(_.equalsIgnoreCase(b)))
    }


    private def isMutuallyBlocked(
        a: String,
        b: String
    ): Boolean = {
      isBlocked(a, b) || isBlocked(b, a)
    }


    private def removeFriendshipBetween(
        a: String,
        b: String
    ): Unit = {
      users.get(a.toLowerCase).foreach { ua =>
        val updated = ua.copy(
          friends = ua.friends.filterNot(_.equalsIgnoreCase(b)),
          incomingRequests = ua.incomingRequests.filterNot(_.equalsIgnoreCase(b))
        )
        users.put(ua.username.toLowerCase, updated)
        saveProfile(updated)
      }
      users.get(b.toLowerCase).foreach { ub =>
        val updated = ub.copy(
          friends = ub.friends.filterNot(_.equalsIgnoreCase(a)),
          incomingRequests = ub.incomingRequests.filterNot(_.equalsIgnoreCase(a))
        )
        users.put(ub.username.toLowerCase, updated)
        saveProfile(updated)
      }
    }


    private def relationBetween(
        viewer: String,
        target: String
    ): String = {

      users.get(target.toLowerCase) match {

        case None =>
          "NONE"

        case Some(targetUser) =>

          if (
            isMutuallyBlocked(viewer, targetUser.username)
          ) {
            "BLOCKED"
          } else if (
            users
              .get(viewer.toLowerCase)
              .exists(_.friends.exists(_.equalsIgnoreCase(targetUser.username)))
          ) {
            "FRIEND"
          } else if (
            targetUser.incomingRequests.exists(_.equalsIgnoreCase(viewer))
          ) {
            "OUTGOING"
          } else if (
            users
              .get(viewer.toLowerCase)
              .exists(_.incomingRequests.exists(_.equalsIgnoreCase(targetUser.username)))
          ) {
            "INCOMING"
          } else {
            "NONE"
          }
      }
    }


    private def ensureDirectChat(
        a: String,
        b: String
    ): String = {

      val chatId =
        directChatId(
          a,
          b
        )

      addMember(
        chatId,
        a
      )

      addMember(
        chatId,
        b
      )

      chatGroups.put(
        chatId,
        false
      )

      chatNames.put(
        chatId,
        ""
      )

      chatHistory.getOrElseUpdate(
        chatId,
        mutable.ListBuffer[ChatMessage]()
      )

      chatId
    }


    private def sendFriendsList(
        username: String
    ): Unit = {

      users
        .get(username.toLowerCase)
        .foreach { user =>

          val friends =
            user.friends
              .flatMap(name => users.get(name.toLowerCase).map(_.username))
              .toSeq
              .sortBy(_.toLowerCase)

          val out =
            new StringBuilder(
              "FRIENDS_LIST|" + friends.length
            )

          friends.foreach { name =>
            val friendUser = users(name.toLowerCase)
            out.append("|").append(Util.encode(friendUser.username))
            out.append("|").append(
              if (sessions.contains(friendUser.username.toLowerCase)) "1" else "0"
            )
            out.append("|").append(Util.encode(friendUser.bio))
          }

          sendToUser(
            username,
            out.toString
          )
        }
    }


    private def sendFriendRequestsList(
        username: String
    ): Unit = {

      users
        .get(username.toLowerCase)
        .foreach { user =>

          val requests =
            user.incomingRequests
              .flatMap(name => users.get(name.toLowerCase).map(_.username))
              .toSeq
              .sortBy(_.toLowerCase)

          val out =
            new StringBuilder(
              "FRIEND_REQUESTS|" + requests.length
            )

          requests.foreach { name =>
            val requester = users(name.toLowerCase)
            out.append("|").append(Util.encode(requester.username))
            out.append("|").append(
              if (sessions.contains(requester.username.toLowerCase)) "1" else "0"
            )
            out.append("|").append(Util.encode(requester.bio))
          }

          sendToUser(
            username,
            out.toString
          )
        }
    }


    // ========================================================
    // DIRECT CHAT ID
    // ========================================================

    private def directChatId(
        a: String,
        b: String
    ): String = {

      val pair =
        Seq(
          a.toLowerCase,
          b.toLowerCase
        ).sorted


      "DM:" +
        pair.head +
        ":" +
        pair(1)
    }


    // ========================================================
    // ADD MEMBER
    // ========================================================

    private def addMember(
        chatId: String,
        username: String
    ): Unit = {

      val set =
        chatMembers.getOrElseUpdate(
          chatId,
          mutable.Set[String]()
        )

      set +=
        username
    }


    // ========================================================
    // SEND TO USER
    // ========================================================

    private def sendToUser(
        username: String,
        line: String
    ): Unit = {

      sessions
        .get(
          username.toLowerCase
        )
        .foreach {
          client =>

            client.send(
              line
            )
        }
    }


    // ========================================================
    // BROADCAST CHAT
    // ========================================================

    private def broadcastChat(
        chatId: String,
        line: String
    ): Unit = {

      chatMembers
        .get(chatId)
        .foreach {
          members =>

            members.foreach {
              username =>

                sendToUser(
                  username,
                  line
                )
            }
        }
    }


    // ========================================================
    // SEND CHAT TO USER
    // ========================================================

    private def addChatToUser(
        chatId: String,
        username: String
    ): Unit = {

      val group =
        chatGroups.getOrElse(
          chatId,
          false
        )


      val title = {

        if (group) {

          "#" +
            chatNames.getOrElse(
              chatId,
              "Group"
            )

        } else {

          val members =
            chatMembers.getOrElse(
              chatId,
              mutable.Set[String]()
            )


          val other =
            members.find {
              x =>
                !x.equalsIgnoreCase(
                  username
                )
            }


          other match {

            case Some(value) =>
              "@" + value

            case None =>
              "@Chat"
          }
        }
      }


      sendToUser(
        username,
        "CHAT|" +
          Util.encode(
            chatId
          ) +
          "|" +
          Util.encode(
            title
          ) +
          "|" +
          (
            if (group) "1"
            else "0"
          )
      )
    }


    // ========================================================
    // SEND ALL USER CHATS
    // ========================================================

    private def sendAllUserChats(
        username: String
    ): Unit = {

      users
        .get(username.toLowerCase)
        .foreach { user =>

          user.friends.foreach { friendName =>
            users.get(friendName.toLowerCase).foreach { friend =>
              val chatId = ensureDirectChat(user.username, friend.username)
              addChatToUser(chatId, user.username)
            }
          }
        }

      chatMembers.keys.foreach {
        chatId =>

          chatMembers
            .get(chatId)
            .foreach { members =>

              if (
                members.contains(
                  username
                )
              ) {

                addChatToUser(
                  chatId,
                  username
                )
              }
            }
        }
    }


    // ========================================================
    // HISTORY
    // ========================================================

    private def sendHistory(
        client: ClientConnection,
        chatId: String
    ): Unit = {

      val list =
        chatHistory.getOrElse(
          chatId,
          mutable.ListBuffer[
            ChatMessage
          ]()
        )


      val start =
        math.max(
          0,
          list.length - 100
        )


      val recent =
        list.slice(
          start,
          list.length
        )


      val out =
        new StringBuilder()


      out.append(
        "HISTORY|"
      )


      out.append(
        Util.encode(
          chatId
        )
      )


      out.append(
        "|" +
          recent.length
      )


      recent.foreach {
        message =>

          out.append(
            "|" +
              Util.encode(
                message.sender
              )
          )


          out.append(
            "|" +
              Util.encode(
                message.time
              )
          )


          out.append(
            "|" +
              Util.encode(
                message.text
              )
          )
      }


      client.send(
        out.toString
      )
    }


    // ========================================================
    // CLIENT LOOP
    // ========================================================

    private def clientLoop(
        client: ClientConnection
    ): Unit = {

      try {

        while (
          !client.socket.isClosed
        ) {

          val line =
            client.reader.readLine()


          if (
            line == null
          ) {

            throw new IOException(
              "Disconnected"
            )
          }


          processCommand(
            client,
            line
          )
        }

      } catch {

        case _: Throwable =>
      }


      lock.synchronized {

        val name =
          client.username


        if (
          name != null &&
          name.nonEmpty
        ) {

          sessions.get(
            name.toLowerCase
          ) match {

            case Some(current)
                if current eq client =>

              sessions.remove(
                name.toLowerCase
              )

              broadcastPresence(
                name
              )

            case _ =>
          }
        }


        loginChallenges.remove(
          client.socket
        )


        registerChallenges.remove(
          client.socket
        )


        recoveryChallenges.remove(
          client.socket
        )
      }


      client.close()
    }


    // ========================================================
    // COMMAND PROCESSOR
    // ========================================================

    private def processCommand(
        client: ClientConnection,
        line: String
    ): Unit = {

      val p =
        line.split(
          "\\|",
          -1
        )


      if (
        p.length == 0
      ) {

        return
      }


      val command =
        p(0)


      // ======================================================
      // REGISTER START
      // ======================================================

      if (
        command ==
          "REGISTER_START"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val username =
          Util.decode(
            p(1)
          ).trim


        if (
          !Util.validUsername(
            username
          )
        ) {

          client.send(
            "ERROR|" +
              Util.encode(
                "Username must be 3-20 characters."
              )
          )

          return
        }


        lock.synchronized {

          if (
            users.contains(
              username.toLowerCase
            )
          ) {

            client.send(
              "ERROR|" +
                Util.encode(
                  "This account already exists. Login instead."
                )
            )

          } else {

            val salt =
              Util.randomHex(
                16
              )


            registerChallenges.put(
              client.socket,
              (
                username,
                salt
              )
            )


            client.send(
              "REGISTER_CHALLENGE|" +
                Util.encode(
                  salt
                )
            )
          }
        }


        return
      }


      // ======================================================
      // REGISTER FINISH
      // ======================================================

      if (
        command ==
          "REGISTER_FINISH"
      ) {

        if (
          p.length < 6
        ) {
          return
        }


        lock.synchronized {

          registerChallenges
            .get(
              client.socket
            ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Registration expired."
                  )
              )


            case Some(
              (
                username,
                salt
              )
              ) =>


              if (
                users.contains(
                  username.toLowerCase
                )
              ) {

                client.send(
                  "ERROR|" +
                    Util.encode(
                      "Account already exists."
                    )
                )

                registerChallenges.remove(
                  client.socket
                )

              } else {

                val passwordVerifier =
                  Util.decode(
                    p(2)
                  )


                val bio =
                  Util.clean(
                    Util.decode(
                      p(3)
                    ),
                    500
                  )


                val avatar =
                  Util.decode(
                    p(4)
                  )


                val validAvatar =
                  if (
                    avatar.length <=
                      480000
                  ) {
                    avatar
                  } else {
                    ""
                  }


                val user =
                  UserRecord(
                    username,
                    salt,
                    passwordVerifier,
                    bio,
                    validAvatar
                  )


                users.put(
                  username.toLowerCase,
                  user
                )


                saveProfile(
                  user
                )


                registerChallenges.remove(
                  client.socket
                )


                client.username =
                  username


                sessions.put(
                  username.toLowerCase,
                  client
                )


                client.send(
                  "LOGIN_OK|" +
                    Util.encode(
                      username
                    )
                )


                sendAllUserChats(
                  username
                )

                sendFriendsList(
                  username
                )

                sendFriendRequestsList(
                  username
                )

                broadcastPresence(
                  username
                )
              }
          }
        }


        return
      }


      // ======================================================
      // LOGIN START
      // ======================================================

      if (
        command ==
          "LOGIN_START"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val username =
          Util.decode(
            p(1)
          ).trim


        lock.synchronized {

          users.get(
            username.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Account not found. Create a new account."
                  )
              )


            case Some(user) =>

              val nonce =
                Util.randomHex(
                  32
                )


              loginChallenges.put(
                client.socket,
                (
                  user.username,
                  nonce
                )
              )


              client.send(
                "LOGIN_CHALLENGE|" +
                  Util.encode(
                    user.passwordSalt
                  ) +
                  "|" +
                  Util.encode(
                    nonce
                  )
              )
          }
        }


        return
      }


      // ======================================================
      // LOGIN FINISH
      // ======================================================

      if (
        command ==
          "LOGIN_FINISH"
      ) {

        if (
          p.length < 3
        ) {
          return
        }


        lock.synchronized {

          loginChallenges
            .get(
              client.socket
            ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Login expired."
                  )
              )


            case Some(
              (
                username,
                nonce
              )
              ) =>


              users.get(
                username.toLowerCase
              ) match {

                case None =>

                  client.send(
                    "ERROR|" +
                      Util.encode(
                        "Account not found."
                      )
                  )


                case Some(user) =>

                  val proof =
                    Util.decode(
                      p(2)
                    )


                  val expected =
                    Util.sha256(
                      user.passwordVerifier +
                        nonce
                    )


                  if (
                    Util.secureEquals(
                      proof,
                      expected
                    )
                  ) {

                    sessions
                      .get(
                        username.toLowerCase
                      )
                      .foreach {
                        old =>
                          old.close()
                      }


                    client.username =
                      username


                    sessions.put(
                      username.toLowerCase,
                      client
                    )


                    loginChallenges.remove(
                      client.socket
                    )


                    client.send(
                      "LOGIN_OK|" +
                        Util.encode(
                          username
                        )
                    )


                    sendAllUserChats(
                      username
                    )

                    sendFriendsList(
                      username
                    )

                    sendFriendRequestsList(
                      username
                    )

                    broadcastPresence(
                      username
                    )

                  } else {

                    loginChallenges.remove(
                      client.socket
                    )


                    client.send(
                      "ERROR|" +
                        Util.encode(
                          "Wrong password."
                        )
                    )
                  }
              }
          }
        }


        return
      }


      // ======================================================
      // FORGOT PASSWORD START
      // ======================================================

      if (
        command ==
          "RECOVERY_START"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val username =
          Util.decode(
            p(1)
          ).trim


        lock.synchronized {

          users.get(
            username.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Recovery failed."
                  )
              )


            case Some(user) =>

              val nonce =
                Util.randomHex(
                  32
                )


              recoveryChallenges.put(
                client.socket,
                (
                  user.username,
                  nonce
                )
              )


              client.send(
                "RECOVERY_CHALLENGE|" +
                  Util.encode(
                    nonce
                  )
              )
          }
        }


        return
      }


      // ======================================================
      // FORGOT PASSWORD FINISH
      // ======================================================

      if (
        command ==
          "RECOVERY_FINISH"
      ) {

        if (
          p.length < 5
        ) {
          return
        }


        lock.synchronized {

          recoveryChallenges
            .get(
              client.socket
            ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Recovery expired."
                  )
              )


            case Some(
              (
                username,
                nonce
              )
              ) =>


              val proof =
                Util.decode(
                  p(2)
                )


              val newSalt =
                Util.decode(
                  p(3)
                )


              val newVerifier =
                Util.decode(
                  p(4)
                )


              val expected =
                Util.sha256(
                  recoveryVerifier +
                    nonce
                )


              if (
                Util.secureEquals(
                  proof,
                  expected
                )
              ) {

                users.get(
                  username.toLowerCase
                ) match {

                  case Some(oldUser) =>

                    val updated =
                      oldUser.copy(
                        passwordSalt =
                          newSalt,
                        passwordVerifier =
                          newVerifier
                      )


                    users.put(
                      username.toLowerCase,
                      updated
                    )


                    saveProfile(
                      updated
                    )


                    client.send(
                      "RECOVERY_OK"
                    )


                  case None =>

                    client.send(
                      "ERROR|" +
                        Util.encode(
                          "Recovery failed."
                        )
                    )
                }

              } else {

                client.send(
                  "ERROR|" +
                    Util.encode(
                      "Recovery failed."
                    )
                )
              }


              recoveryChallenges.remove(
                client.socket
              )
          }
        }


        return
      }


      // ======================================================
      // REQUIRE LOGIN
      // ======================================================

      val currentUser =
        client.username


      if (
        currentUser == null ||
        currentUser.isEmpty
      ) {

        client.send(
          "ERROR|" +
            Util.encode(
              "Please login first."
            )
        )

        return
      }


      // ======================================================
      // SEARCH PEOPLE
      // ======================================================

      if (
        command ==
          "SEARCH"
      ) {

        val query =
          if (
            p.length >= 2
          ) {

            Util.decode(
              p(1)
            ).trim.toLowerCase

          } else {

            ""
          }


        val found =
          users.values
            .filter {
              user =>

                user.username
                  .toLowerCase
                  .contains(
                    query
                  ) &&
                !user.username
                  .equalsIgnoreCase(
                    currentUser
                  )
            }
            .toSeq
            .sortBy(
              _.username.toLowerCase
            )
            .take(100)


        val result =
          new StringBuilder()


        result.append(
          "SEARCH_RESULT|" +
            found.length
        )


        found.foreach {
          user =>

            result.append(
              "|" +
                Util.encode(
                  user.username
                )
            )


            result.append(
              "|" +
                Util.encode(
                  user.bio
                )
            )


            result.append(
              "|" +
                (
                  if (
                    sessions.contains(
                      user.username.toLowerCase
                    )
                  ) {
                    "1"
                  } else {
                    "0"
                  }
                )
            )

            result.append(
              "|" +
                relationBetween(
                  currentUser,
                  user.username
                )
            )
        }


        client.send(
          result.toString
        )


        return
      }


      // ======================================================
      // GET PROFILE
      // ======================================================

      if (
        command ==
          "PROFILE"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val target =
          Util.decode(
            p(1)
          ).trim


        users.get(
          target.toLowerCase
        ) match {

          case None =>

            client.send(
              "ERROR|" +
                Util.encode(
                  "Profile not found."
                )
            )


          case Some(user) =>

            client.send(
              "PROFILE_DATA|" +
                Util.encode(
                  user.username
                ) +
                "|" +
                Util.encode(
                  user.bio
                ) +
                "|" +
                Util.encode(
                  user.avatar
                ) +
                "|" +
                (
                  if (
                    sessions.contains(
                      user.username.toLowerCase
                    )
                  ) {
                    "1"
                  } else {
                    "0"
                  }
                ) +
                "|" +
                relationBetween(
                  currentUser,
                  user.username
                )
            )
        }


        return
      }


      // ======================================================
      // UPDATE PROFILE
      // ======================================================

      if (
        command ==
          "UPDATE_PROFILE"
      ) {

        if (
          p.length < 3
        ) {
          return
        }


        lock.synchronized {

          users.get(
            currentUser.toLowerCase
          ) match {

            case None =>

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Profile not found."
                  )
              )


            case Some(oldUser) =>

              val bio =
                Util.clean(
                  Util.decode(
                    p(1)
                  ),
                  500
                )


              val avatar =
                Util.decode(
                  p(2)
                )


              val finalAvatar =
                if (
                  avatar.length <=
                    480000
                ) {
                  avatar
                } else {
                  oldUser.avatar
                }


              val updated =
                oldUser.copy(
                  bio =
                    bio,
                  avatar =
                    finalAvatar
                )


              users.put(
                currentUser.toLowerCase,
                updated
              )


              saveProfile(
                updated
              )


              client.send(
                "PROFILE_SAVED"
              )


              onlineUsersNotify(
                currentUser
              )
          }
        }


        return
      }


      // ======================================================
      // OPEN DIRECT CHAT
      // ======================================================

      if (
        command ==
          "OPEN_DM"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val targetName =
          Util.decode(
            p(1)
          ).trim


        users.get(
          targetName.toLowerCase
        ) match {

          case None =>

            client.send(
              "ERROR|" +
                Util.encode(
                  "User does not exist."
                )
            )


          case Some(targetUser) =>

            if (
              targetUser.username.equalsIgnoreCase(
                currentUser
              )
            ) {

              client.send(
                "ERROR|" +
                  Util.encode(
                    "You cannot chat with yourself."
                  )
              )

            } else if (
              isMutuallyBlocked(
                currentUser,
                targetUser.username
              )
            ) {

              client.send(
                "ERROR|" +
                  Util.encode(
                    "You cannot open this chat because one of the users is blocked."
                  )
              )

            } else if (
              !isFriend(
                currentUser,
                targetUser.username
              )
            ) {

              client.send(
                "ERROR|" +
                  Util.encode(
                    "Become friends first: send and accept a friend request."
                  )
              )

            } else {

              lock.synchronized {

                val chatId =
                  ensureDirectChat(
                    currentUser,
                    targetUser.username
                  )

                addChatToUser(
                  chatId,
                  currentUser
                )

                addChatToUser(
                  chatId,
                  targetUser.username
                )

                sendHistory(
                  client,
                  chatId
                )
              }
            }
        }


        return
      }


      // ======================================================
      // TYPING INDICATOR
      // ======================================================

      if (
        command ==
          "TYPING"
      ) {

        if (p.length < 3) return

        val chatId = Util.decode(p(1))
        val state = if (p(2) == "1") "1" else "0"

        lock.synchronized {
          val allowed = chatMembers.get(chatId).exists(_.contains(currentUser))
          if (allowed) {
            broadcastChat(
              chatId,
              "TYPING|" +
                Util.encode(chatId) +
                "|" +
                Util.encode(currentUser) +
                "|" +
                state
            )
          }
        }
        return
      }


      // ======================================================
      // BLOCK / UNBLOCK
      // ======================================================

      if (command == "BLOCK_USER" || command == "UNBLOCK_USER") {

        if (p.length < 2) return

        val targetName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(targetName.toLowerCase) match {
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))

            case Some(targetUser) =>
              if (targetUser.username.equalsIgnoreCase(currentUser)) {
                client.send("ERROR|" + Util.encode("You cannot block yourself."))
              } else {
                users.get(currentUser.toLowerCase).foreach { me =>
                  if (command == "BLOCK_USER") {
                    val updatedMe = me.copy(
                      blocked = me.blocked + targetUser.username,
                      friends = me.friends.filterNot(_.equalsIgnoreCase(targetUser.username)),
                      incomingRequests = me.incomingRequests.filterNot(_.equalsIgnoreCase(targetUser.username))
                    )
                    users.put(me.username.toLowerCase, updatedMe)
                    saveProfile(updatedMe)

                    val updatedTarget = targetUser.copy(
                      friends = targetUser.friends.filterNot(_.equalsIgnoreCase(me.username)),
                      incomingRequests = targetUser.incomingRequests.filterNot(_.equalsIgnoreCase(me.username))
                    )
                    users.put(targetUser.username.toLowerCase, updatedTarget)
                    saveProfile(updatedTarget)

                    client.send("BLOCK_STATE|" + Util.encode(targetUser.username) + "|1")
                    sendToUser(targetUser.username, "BLOCKED_BY|" + Util.encode(me.username))
                    sendFriendsList(me.username)
                    sendFriendsList(targetUser.username)
                    sendFriendRequestsList(me.username)
                    sendFriendRequestsList(targetUser.username)
                    searchNotifyAll()
                  } else {
                    val updatedMe = me.copy(
                      blocked = me.blocked.filterNot(_.equalsIgnoreCase(targetUser.username))
                    )
                    users.put(me.username.toLowerCase, updatedMe)
                    saveProfile(updatedMe)
                    client.send("BLOCK_STATE|" + Util.encode(targetUser.username) + "|0")
                    sendToUser(targetUser.username, "UNBLOCKED_BY|" + Util.encode(me.username))
                    searchNotifyAll()
                  }
                }
              }
          }
        }
        return
      }


      // ======================================================
      // FRIEND REQUEST
      // ======================================================

      if (
        command ==
          "FRIEND_REQUEST"
      ) {

        if (p.length < 2) return

        val targetName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(targetName.toLowerCase) match {
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))

            case Some(targetUser) =>
              if (targetUser.username.equalsIgnoreCase(currentUser)) {
                client.send("ERROR|" + Util.encode("You cannot add yourself."))
              } else if (isMutuallyBlocked(currentUser, targetUser.username)) {
                client.send("ERROR|" + Util.encode("Friend request cannot be sent because one of the users is blocked."))
              } else if (isFriend(currentUser, targetUser.username)) {
                client.send("ERROR|" + Util.encode("You are already friends."))
              } else if (targetUser.incomingRequests.exists(_.equalsIgnoreCase(currentUser))) {
                client.send("ERROR|" + Util.encode("Friend request already sent."))
              } else if (users.get(currentUser.toLowerCase).exists(_.incomingRequests.exists(_.equalsIgnoreCase(targetUser.username)))) {
                client.send("ERROR|" + Util.encode("This person already sent you a request. Accept it instead."))
              } else {
                val updatedTarget = targetUser.copy(
                  incomingRequests = targetUser.incomingRequests + currentUser
                )
                users.put(targetUser.username.toLowerCase, updatedTarget)
                saveProfile(updatedTarget)

                client.send(
                  "FRIEND_REQUEST_SENT|" + Util.encode(targetUser.username)
                )

                sendToUser(
                  targetUser.username,
                  "FRIEND_REQUEST_RECEIVED|" + Util.encode(currentUser)
                )
              }
          }
        }

        return
      }


      // ======================================================
      // FRIEND ACCEPT
      // ======================================================

      if (
        command ==
          "FRIEND_ACCEPT"
      ) {

        if (p.length < 2) return

        val requesterName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(requesterName.toLowerCase) match {
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))

            case Some(requester) =>
              users.get(currentUser.toLowerCase) match {
                case None =>
                  client.send("ERROR|" + Util.encode("Current account not found."))

                case Some(me) =>
                  val requestExists = me.incomingRequests.exists(_.equalsIgnoreCase(requester.username))
                  if (!requestExists) {
                    client.send("ERROR|" + Util.encode("No pending friend request from this person."))
                  } else {
                    val updatedMe = me.copy(
                      friends = me.friends + requester.username,
                      incomingRequests = me.incomingRequests.filterNot(_.equalsIgnoreCase(requester.username))
                    )
                    val updatedRequester = requester.copy(
                      friends = requester.friends + me.username
                    )

                    users.put(me.username.toLowerCase, updatedMe)
                    users.put(requester.username.toLowerCase, updatedRequester)
                    saveProfile(updatedMe)
                    saveProfile(updatedRequester)

                    val chatId = ensureDirectChat(me.username, requester.username)
                    addChatToUser(chatId, me.username)
                    addChatToUser(chatId, requester.username)

                    client.send(
                      "FRIEND_ACCEPTED|" + Util.encode(requester.username)
                    )
                    sendToUser(
                      requester.username,
                      "FRIEND_ACCEPTED|" + Util.encode(me.username)
                    )

                    sendFriendsList(me.username)
                    sendFriendRequestsList(me.username)
                    sendFriendsList(requester.username)
                    sendFriendRequestsList(requester.username)
                  }
              }
          }
        }

        return
      }


      // ======================================================
      // FRIEND REJECT
      // ======================================================

      if (
        command ==
          "FRIEND_REJECT"
      ) {

        if (p.length < 2) return

        val requesterName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(currentUser.toLowerCase) match {
            case Some(me) =>
              val filtered = me.incomingRequests.filterNot(_.equalsIgnoreCase(requesterName))
              val updated = me.copy(incomingRequests = filtered)
              users.put(me.username.toLowerCase, updated)
              saveProfile(updated)
              sendFriendRequestsList(me.username)
              sendToUser(
                requesterName,
                "FRIEND_REQUEST_REJECTED|" + Util.encode(me.username)
              )
            case None =>
          }
        }

        return
      }


      // ======================================================
      // REMOVE FRIEND
      // ======================================================

      if (
        command ==
          "FRIEND_REMOVE"
      ) {

        if (p.length < 2) return

        val targetName = Util.decode(p(1)).trim

        lock.synchronized {
          users.get(targetName.toLowerCase) match {
            case Some(targetUser) =>
              users.get(currentUser.toLowerCase) match {
                case Some(me) =>
                  val updatedMe = me.copy(
                    friends = me.friends.filterNot(_.equalsIgnoreCase(targetUser.username))
                  )
                  val updatedTarget = targetUser.copy(
                    friends = targetUser.friends.filterNot(_.equalsIgnoreCase(me.username))
                  )
                  users.put(me.username.toLowerCase, updatedMe)
                  users.put(targetUser.username.toLowerCase, updatedTarget)
                  saveProfile(updatedMe)
                  saveProfile(updatedTarget)
                  client.send("FRIEND_REMOVED|" + Util.encode(targetUser.username))
                  sendToUser(targetUser.username, "FRIEND_REMOVED|" + Util.encode(me.username))
                  sendFriendsList(me.username)
                  sendFriendsList(targetUser.username)
                case None =>
              }
            case None =>
              client.send("ERROR|" + Util.encode("User does not exist."))
          }
        }

        return
      }


      // ======================================================
      // FRIENDS LIST
      // ======================================================

      if (
        command ==
          "FRIENDS_LIST"
      ) {
        lock.synchronized {
          sendFriendsList(currentUser)
        }
        return
      }


      // ======================================================
      // FRIEND REQUESTS LIST
      // ======================================================

      if (
        command ==
          "FRIEND_REQUESTS"
      ) {
        lock.synchronized {
          sendFriendRequestsList(currentUser)
        }
        return
      }


      // ======================================================
      // CREATE GROUP
      // ======================================================

      if (
        command ==
          "CREATE_GROUP"
      ) {

        if (
          p.length < 3
        ) {
          return
        }


        val groupName =
          Util.clean(
            Util.decode(
              p(1)
            ),
            40
          )


        val count =
          try {
            p(2).toInt
          } catch {
            case _: Throwable =>
              0
          }


        if (
          groupName.isEmpty ||
          count < 1 ||
          count > 20 ||
          p.length <
            3 + count
        ) {

          client.send(
            "ERROR|" +
              Util.encode(
                "Invalid group."
              )
          )

          return
        }


        lock.synchronized {

          val members =
            mutable.Set[String]()


          members +=
            currentUser


          var i =
            0


          while (
            i < count
          ) {

            val name =
              Util.decode(
                p(
                  3 + i
                )
              ).trim


            if (
              users.contains(
                name.toLowerCase
              ) &&
              !name.equalsIgnoreCase(
                currentUser
              )
            ) {

              members +=
                users(
                  name.toLowerCase
                ).username
            }


            i +=
              1
          }


          if (
            members.size < 2
          ) {

            client.send(
              "ERROR|" +
                Util.encode(
                  "At least one other person is required."
                )
            )


          } else {

            val chatId =
              "GROUP:" +
                UUID.randomUUID()
                  .toString


            chatMembers.put(
              chatId,
              members
            )


            chatGroups.put(
              chatId,
              true
            )


            chatNames.put(
              chatId,
              groupName
            )


            chatHistory.put(
              chatId,
              mutable.ListBuffer[
                ChatMessage
              ]()
            )


            members.foreach {
              member =>

                addChatToUser(
                  chatId,
                  member
                )
            }


            client.send(
              "GROUP_CREATED|" +
                Util.encode(
                  groupName
                )
            )
          }
        }


        return
      }


      // ======================================================
      // OPEN CHAT
      // ======================================================

      if (
        command ==
          "OPEN_CHAT"
      ) {

        if (
          p.length < 2
        ) {
          return
        }


        val chatId =
          Util.decode(
            p(1)
          )


        val allowed =
          chatMembers
            .get(chatId)
            .exists {
              members =>
                members.contains(
                  currentUser
                )
            }


        if (
          allowed
        ) {

          sendHistory(
            client,
            chatId
          )

        } else {

          client.send(
            "ERROR|" +
              Util.encode(
                "You are not a member of this chat."
              )
          )
        }


        return
      }


      // ======================================================
      // SEND MESSAGE
      // ======================================================

      if (
        command ==
          "SEND_MESSAGE"
      ) {

        if (
          p.length < 3
        ) {
          return
        }


        val chatId =
          Util.decode(
            p(1)
          )


        val message =
          Util.clean(
            Util.decode(
              p(2)
            ),
            2000
          )


        val allowed =
          chatMembers
            .get(chatId)
            .exists {
              members =>
                members.contains(
                  currentUser
                )
            }

        val directBlocked =
          if (chatId.startsWith("DM:")) {
            chatMembers.get(chatId).exists { members =>
              members.exists { member =>
                !member.equalsIgnoreCase(currentUser) &&
                isMutuallyBlocked(currentUser, member)
              }
            }
          } else {
            false
          }


        if (
          allowed &&
          !directBlocked &&
          message.nonEmpty
        ) {

          val item =
            ChatMessage(
              currentUser,
              message,
              Util.now()
            )


          val list =
            chatHistory.getOrElseUpdate(
              chatId,
              mutable.ListBuffer[
                ChatMessage
              ]()
            )


          list +=
            item


          while (
            list.length > 300
          ) {

            list.remove(0)
          }


          broadcastChat(
            chatId,
            "MESSAGE|" +
              Util.encode(
                chatId
              ) +
              "|" +
              Util.encode(
                item.sender
              ) +
              "|" +
              Util.encode(
                item.time
              ) +
              "|" +
              Util.encode(
                item.text
              )
          )
        }


        return
      }


      // ======================================================
      // LOGOUT
      // ======================================================

      if (command == "LOGOUT") {
        lock.synchronized {
          val name = client.username
          if (name != null && name.nonEmpty) {
            sessions.get(name.toLowerCase) match {
              case Some(current) if current eq client =>
                sessions.remove(name.toLowerCase)
                broadcastPresence(name)
              case _ =>
            }
          }
        }
        client.send("LOGOUT_OK")
        client.close()
        return
      }


      // ======================================================
      // PING
      // ======================================================

      if (
        command ==
          "PING"
      ) {

        client.send(
          "PONG"
        )

        return
      }
    }


    private def searchNotifyAll(): Unit = {
      sessions.values.foreach { c =>
        if (c.username != null && c.username.nonEmpty) {
          c.send("PROFILE_CHANGED|" + Util.encode(c.username))
        }
      }
    }


    // ========================================================
    // PRESENCE
    // ========================================================

    private def broadcastPresence(
        username: String
    ): Unit = {

      val state =
        if (
          sessions.contains(
            username.toLowerCase
          )
        ) {
          "1"
        } else {
          "0"
        }


      val line =
        "PRESENCE|" +
          Util.encode(
            username
          ) +
          "|" +
          state


      sessions.values.foreach {
        client =>

          client.send(
            line
          )

          if (client.username != null && client.username.nonEmpty) {
            sendFriendsList(client.username)
          }
      }
    }


    // ========================================================
    // PROFILE NOTIFICATION
    // ========================================================

    private def onlineUsersNotify(
        username: String
    ): Unit = {

      sessions.values.foreach {
        client =>

          client.send(
            "PROFILE_CHANGED|" +
              Util.encode(
                username
              )
          )
      }
    }
  }


  // ==========================================================
  // CLIENT
  // ==========================================================

  class ChatClient {

    var socket:
        Socket = null

    var reader:
        BufferedReader = null

    var writer:
        PrintWriter = null

    var connected =
      false

    @volatile private var heartbeatRunning =
      false

    @volatile private var intentionalClose =
      false


    var username =
      ""

    var password =
      ""


    var registerUsername =
      ""

    var registerPassword =
      ""

    var registerBio =
      ""

    var registerAvatar =
      ""


    var recoveryUsername =
      ""

    var recoverySecret =
      ""

    var recoveryNewPassword =
      ""


    var ownBio =
      ""

    var ownAvatar =
      ""


    var callback:
        String => Unit =
        (_: String) => {}


    // ========================================================
    // CONNECT
    // ========================================================

    def connect(
        host: String,
        port: Int
    ): Boolean = {

      intentionalClose = false

      if (
        connected
      ) {
        return true
      }


      try {

        socket =
          new Socket()


        socket.setKeepAlive(
          true
        )


        socket.setTcpNoDelay(
          true
        )


        socket.connect(
          new InetSocketAddress(
            host,
            port
          ),
          7000
        )


        reader =
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream,
              StandardCharsets.UTF_8
            )
          )


        writer =
          new PrintWriter(
            new OutputStreamWriter(
              socket.getOutputStream,
              StandardCharsets.UTF_8
            ),
            true
          )


        connected =
          true


        val thread =
          new Thread(
            new Runnable {

              override def run(): Unit = {

                readLoop()
              }
            }
          )


        thread.setDaemon(
          true
        )


        thread.start()

        startHeartbeat()


        true

      } catch {

        case _: Throwable =>

          connected =
            false

          false
      }
    }


    private def startHeartbeat(): Unit = {
      if (heartbeatRunning) return
      heartbeatRunning = true
      val t = new Thread(
        new Runnable {
          override def run(): Unit = {
            while (heartbeatRunning) {
              try {
                Thread.sleep(15000L)
              } catch {
                case _: Throwable =>
              }
              if (connected) {
                send("PING")
              }
            }
          }
        },
        "Yadnesh-Heartbeat"
      )
      t.setDaemon(true)
      t.start()
    }


    // ========================================================
    // SEND
    // ========================================================

    def send(
        line: String
    ): Unit = {

      if (
        connected &&
        writer != null
      ) {

        try {

          writer.println(
            line
          )

          writer.flush()

        } catch {

          case _: Throwable =>
        }
      }
    }


    // ========================================================
    // CLOSE
    // ========================================================

    def close(): Unit = {

      intentionalClose = true

      heartbeatRunning =
        false

      connected =
        false

      try {

        if (
          socket != null
        ) {
          socket.close()
        }

      } catch {

        case _: Throwable =>
      }
    }


    // ========================================================
    // READ LOOP
    // ========================================================

    private def readLoop():
        Unit = {

      try {

        while (
          connected
        ) {

          val line =
            reader.readLine()


          if (
            line == null
          ) {

            throw new IOException(
              "Disconnected"
            )
          }


          val message =
            line


          SwingUtilities.invokeLater(
            new Runnable {

              override def run(): Unit = {

                processEvent(
                  message
                )
              }
            }
          )
        }

      } catch {

        case _: Throwable =>

          connected =
            false

          heartbeatRunning =
            false

          if (!intentionalClose) {
            try {
              SwingUtilities.invokeLater(
                new Runnable {
                  override def run(): Unit = {
                    callback("CLIENT_DISCONNECTED|" + Util.encode("Server connection lost."))
                  }
                }
              )
            } catch {
              case _: Throwable =>
            }
          }
      }
    }


    // ========================================================
    // EVENTS
    // ========================================================

    private def processEvent(
        line: String
    ): Unit = {

      val p =
        line.split(
          "\\|",
          -1
        )


      if (
        p.length == 0
      ) {
        return
      }


      p(0) match {

        // ----------------------------------------------------
        // LOGIN CHALLENGE
        // ----------------------------------------------------

        case "LOGIN_CHALLENGE" =>

          if (
            p.length >= 3
          ) {

            val salt =
              Util.decode(
                p(1)
              )


            val nonce =
              Util.decode(
                p(2)
              )


            val verifier =
              Util.sha256(
                salt +
                  password
              )


            val proof =
              Util.sha256(
                verifier +
                  nonce
              )


            send(
              "LOGIN_FINISH|" +
                Util.encode(
                  username
                ) +
                "|" +
                Util.encode(
                  proof
                )
            )
          }


        // ----------------------------------------------------
        // REGISTER CHALLENGE
        // ----------------------------------------------------

        case "REGISTER_CHALLENGE" =>

          if (
            p.length >= 2
          ) {

            val salt =
              Util.decode(
                p(1)
              )


            val verifier =
              Util.sha256(
                salt +
                  registerPassword
              )


            // Empty field kept for protocol compatibility.
            send(
              "REGISTER_FINISH|" +
                Util.encode(
                  registerUsername
                ) +
                "|" +
                Util.encode(
                  verifier
                ) +
                "|" +
                Util.encode(
                  registerBio
                ) +
                "|" +
                Util.encode(
                  registerAvatar
                ) +
                "|" +
                Util.encode(
                  "END"
                )
            )
          }


        // ----------------------------------------------------
        // RECOVERY CHALLENGE
        // ----------------------------------------------------

        case "RECOVERY_CHALLENGE" =>

          if (
            p.length >= 2
          ) {

            val nonce =
              Util.decode(
                p(1)
              )


            val recoveryHash =
              Util.sha256(
                recoverySecret
              )


            val proof =
              Util.sha256(
                recoveryHash +
                  nonce
              )


            val newSalt =
              Util.randomHex(
                16
              )


            val newVerifier =
              Util.sha256(
                newSalt +
                  recoveryNewPassword
              )


            send(
              "RECOVERY_FINISH|" +
                Util.encode(
                  recoveryUsername
                ) +
                "|" +
                Util.encode(
                  proof
                ) +
                "|" +
                Util.encode(
                  newSalt
                ) +
                "|" +
                Util.encode(
                  newVerifier
                )
            )


            recoverySecret =
              ""

            recoveryNewPassword =
              ""
          }


        // ----------------------------------------------------
        // LOGIN OK
        // ----------------------------------------------------

        case "LOGIN_OK" =>

          if (
            p.length >= 2
          ) {

            username =
              Util.decode(
                p(1)
              )


            password =
              ""


            callback(
              line
            )
          }


        case "RECOVERY_OK" =>
          callback(line)


        case "SEARCH_RESULT" =>
          callback(line)


        case "PROFILE_DATA" =>
          callback(line)


        case "PROFILE_SAVED" =>
          callback(line)


        case "PROFILE_CHANGED" =>
          callback(line)


        case "CHAT" =>
          callback(line)


        case "HISTORY" =>
          callback(line)


        case "MESSAGE" =>
          callback(line)


        case "PRESENCE" =>
          callback(line)


        case "FRIENDS_LIST" =>
          callback(line)


        case "FRIEND_REQUESTS" =>
          callback(line)


        case "FRIEND_REQUEST_SENT" =>
          callback(line)


        case "FRIEND_REQUEST_RECEIVED" =>
          callback(line)


        case "FRIEND_ACCEPTED" =>
          callback(line)


        case "FRIEND_REQUEST_REJECTED" =>
          callback(line)


        case "FRIEND_REMOVED" =>
          callback(line)


        case "TYPING" =>
          callback(line)


        case "BLOCK_STATE" =>
          callback(line)


        case "BLOCKED_BY" =>
          callback(line)


        case "UNBLOCKED_BY" =>
          callback(line)


        case "LOGOUT_OK" =>
          callback(line)


        case "CLIENT_DISCONNECTED" =>
          callback(line)


        case "PONG" =>
          ()


        case "GROUP_CREATED" =>
          callback(line)


        case "ERROR" =>

          val text =
            if (
              p.length >= 2
            ) {

              Util.decode(
                p(1)
              )

            } else {

              "Unknown error."
            }


          JOptionPane.showMessageDialog(
            null,
            text,
            APP_NAME,
            JOptionPane.ERROR_MESSAGE
          )


        case _ =>
      }
    }
  }


  // ==========================================================
  // LOGIN FRAME
  // ==========================================================

  class LoginFrame {

    private val frame =
      new JFrame(
        APP_NAME
      )

    private val client =
      new ChatClient

    private val usernameField =
      new JTextField()

    private val passwordField =
      new JPasswordField()

    private val networkStatus =
      new JLabel(
        "NETWORK: STARTING...",
        SwingConstants.CENTER
      )

    @volatile private var networkReady =
      false

    private var discoveredHost =
      "127.0.0.1"

    private var discoveredPort =
      DEFAULT_PORT


    // ========================================================
    // SHOW
    // ========================================================

    def show(): Unit = {

      Util.setupFont()

      frame.setTitle(
        APP_NAME +
          " - Login"
      )

      frame.setDefaultCloseOperation(
        WindowConstants.EXIT_ON_CLOSE
      )

      frame.setSize(
        760,
        560
      )

      frame.setLocationRelativeTo(
        null
      )

      val root =
        new JPanel(
          new BorderLayout(
            15,
            15
          )
        )

      root.setBorder(
        BorderFactory.createEmptyBorder(
          20,
          20,
          20,
          20
        )
      )


      // ------------------------------------------------------
      // TITLE
      // ------------------------------------------------------

      val title =
        new JLabel(
          "<html>" +
            "<center>" +
            "<font size='6'><b>YADNESH LIVE CHAT</b></font><br>" +
            "<font size='3'>Automatic Wi-Fi PC-to-PC Live Chat</font>" +
            "</center>" +
            "</html>",
          SwingConstants.CENTER
        )

      root.add(
        title,
        BorderLayout.NORTH
      )


      // ------------------------------------------------------
      // LOGIN FORM
      // ------------------------------------------------------

      val form =
        new JPanel(
          new GridLayout(
            2,
            2,
            10,
            10
          )
        )

      form.setBorder(
        BorderFactory.createEmptyBorder(
          30,
          30,
          20,
          30
        )
      )

      form.add(
        new JLabel(
          "Username / ID:"
        )
      )

      form.add(
        usernameField
      )

      form.add(
        new JLabel(
          "Password:"
        )
      )

      form.add(
        passwordField
      )

      val center =
        new JPanel(
          new BorderLayout(
            8,
            8
          )
        )

      center.add(
        form,
        BorderLayout.CENTER
      )

      networkStatus.setBorder(
        BorderFactory.createEmptyBorder(
          8,
          8,
          8,
          8
        )
      )

      center.add(
        networkStatus,
        BorderLayout.SOUTH
      )

      root.add(
        center,
        BorderLayout.CENTER
      )


      // ------------------------------------------------------
      // BUTTONS
      // ------------------------------------------------------

      val loginButton =
        new JButton(
          "LOGIN"
        )

      val registerButton =
        new JButton(
          "CREATE NEW ACCOUNT"
        )

      val forgotButton =
        new JButton(
          "FORGOT PASSWORD"
        )

      val buttons =
        new JPanel(
          new GridLayout(
            1,
            3,
            8,
            8
          )
        )

      buttons.add(
        loginButton
      )

      buttons.add(
        registerButton
      )

      buttons.add(
        forgotButton
      )

      root.add(
        buttons,
        BorderLayout.SOUTH
      )


      // ======================================================
      // LOGIN
      // ======================================================

      loginButton.addActionListener(
        new ActionListener {

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

            doLogin()
          }
        }
      )


      // ======================================================
      // REGISTER
      // ======================================================

      registerButton.addActionListener(
        new ActionListener {

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

            doRegister()
          }
        }
      )


      // ======================================================
      // FORGOT PASSWORD
      // ======================================================

      forgotButton.addActionListener(
        new ActionListener {

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

            forgotPassword()
          }
        }
      )


      frame.setContentPane(
        root
      )

      frame.setVisible(
        true
      )

      startAutomaticNetwork()
    }


    // ========================================================
    // AUTOMATIC NETWORK
    // ========================================================

    private def startAutomaticNetwork(): Unit = {

      networkStatus.setText(
        "NETWORK: SEARCHING FOR HOST PC..."
      )

      val thread =
        new Thread(
          new Runnable {

            override def run(): Unit = {

              var found:
                  Option[(String, Int)] =
                None

              val deadline =
                System.currentTimeMillis() +
                  2500L

              while (
                found.isEmpty &&
                System.currentTimeMillis() <
                  deadline
              ) {

                found =
                  LanDiscovery.findServer(
                    700
                  )
              }

              found match {

                case Some(
                    (host, port)
                    ) =>

                  discoveredHost =
                    host

                  discoveredPort =
                    port

                  val ok =
                    client.connect(
                      host,
                      port
                    )

                  networkReady =
                    ok

                  SwingUtilities.invokeLater(
                    new Runnable {

                      override def run(): Unit = {

                        if (ok) {

                          networkStatus.setText(
                            "NETWORK: CONNECTED AUTOMATICALLY ? " +
                              host +
                              ":" +
                              port
                          )

                        } else {

                          networkStatus.setText(
                            "NETWORK: HOST FOUND BUT CONNECTION FAILED"
                          )
                        }
                      }
                    }
                  )

                case None =>

                  // No host was found, so this PC becomes
                  // the first host automatically.
                  val started =
                    Server.start(
                      DEFAULT_PORT
                    )

                  if (
                    started
                  ) {

                    discoveredHost =
                      "127.0.0.1"

                    discoveredPort =
                      DEFAULT_PORT

                    val ok =
                      client.connect(
                        "127.0.0.1",
                        DEFAULT_PORT
                      )

                    networkReady =
                      ok

                    SwingUtilities.invokeLater(
                      new Runnable {

                        override def run(): Unit = {

                          if (ok) {

                            networkStatus.setText(
                              "NETWORK: THIS PC IS HOST ? " +
                                Util.localIPs().mkString(
                                  ", "
                                ) +
                                ":" +
                                DEFAULT_PORT
                            )

                          } else {

                            networkStatus.setText(
                              "NETWORK: HOST STARTED BUT LOCAL CONNECTION FAILED"
                            )
                          }
                        }
                      }
                    )

                  } else {

                    // A server may have appeared just after the
                    // discovery timeout. Try discovery once more.
                    val retry =
                      LanDiscovery.findServer(
                        1500
                      )

                    retry match {

                      case Some(
                          (host, port)
                          ) =>

                        discoveredHost =
                          host

                        discoveredPort =
                          port

                        val ok =
                          client.connect(
                            host,
                            port
                          )

                        networkReady =
                          ok

                        SwingUtilities.invokeLater(
                          new Runnable {

                            override def run(): Unit = {

                              networkStatus.setText(
                                if (ok) {
                                  "NETWORK: CONNECTED ? " +
                                    host +
                                    ":" +
                                    port
                                } else {
                                  "NETWORK: CONNECTION FAILED"
                                }
                              )
                            }
                          }
                        )

                      case None =>

                        networkReady =
                          false

                        SwingUtilities.invokeLater(
                          new Runnable {

                            override def run(): Unit = {

                              networkStatus.setText(
                                "NETWORK: NOT AVAILABLE ? START HOST ON ONE PC"
                              )
                            }
                          }
                        )
                    }
                  }
              }
            }
          },
          "Yadnesh-Auto-Network"
        )

      thread.setDaemon(
        true
      )

      thread.start()
    }


    // ========================================================
    // CONNECT
    // ========================================================

    private def connect(): Boolean = {

      if (
        client.connected
      ) {
        return true
      }

      if (
        !networkReady
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Network is still starting. Please wait a moment.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return false
      }

      val ok =
        client.connect(
          discoveredHost,
          discoveredPort
        )

      if (
        !ok
      ) {

        networkReady =
          false

        networkStatus.setText(
          "NETWORK: DISCONNECTED"
        )
      }

      ok
    }


    // ========================================================
    // LOGIN
    // ========================================================

    private def doLogin(): Unit = {

      val name =
        usernameField
          .getText
          .trim

      val pass =
        new String(
          passwordField
            .getPassword
        )

      if (
        !Util.validUsername(
          name
        )
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Username must be 3-20 characters.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }

      if (
        pass.isEmpty
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Enter password.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }

      if (
        connect()
      ) {

        client.username =
          name

        client.password =
          pass

        client.callback =
          loginEvent

        client.send(
          "LOGIN_START|" +
            Util.encode(
              name
            )
        )
      }
    }


    // ========================================================
    // LOGIN EVENT
    // ========================================================

    private def loginEvent(
        line: String
    ): Unit = {

      if (
        line.startsWith(
          "LOGIN_OK|"
        )
      ) {

        openMain()
      }
    }


    // ========================================================
    // REGISTER
    // ========================================================

    private def doRegister(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "CREATE NEW ACCOUNT",
          true
        )

      dialog.setSize(
        680,
        560
      )

      dialog.setLocationRelativeTo(
        frame
      )

      val username =
        new JTextField()

      val password =
        new JPasswordField()

      val confirm =
        new JPasswordField()

      val bio =
        new JTextField(
          "Hello! I use Yadnesh Live Chat."
        )

      val choose =
        new JButton(
          "CHOOSE IMAGE"
        )

      val paste =
        new JButton(
          "PASTE IMAGE"
        )

      val imageLabel =
        new JLabel(
          "No image selected"
        )

      var avatar =
        ""


      val form =
        new JPanel(
          new GridLayout(
            6,
            2,
            8,
            8
          )
        )

      form.setBorder(
        BorderFactory.createEmptyBorder(
          15,
          15,
          15,
          15
        )
      )

      form.add(
        new JLabel(
          "Username / ID:"
        )
      )

      form.add(
        username
      )

      form.add(
        new JLabel(
          "Password:"
        )
      )

      form.add(
        password
      )

      form.add(
        new JLabel(
          "Confirm Password:"
        )
      )

      form.add(
        confirm
      )

      form.add(
        new JLabel(
          "Public Bio:"
        )
      )

      form.add(
        bio
      )

      form.add(
        new JLabel(
          "Profile Image:"
        )
      )

      val imagePanel =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )

      val imageButtons =
        new JPanel(
          new GridLayout(
            1,
            2,
            5,
            5
          )
        )

      imageButtons.add(
        choose
      )

      imageButtons.add(
        paste
      )

      imagePanel.add(
        imageButtons,
        BorderLayout.WEST
      )

      imagePanel.add(
        imageLabel,
        BorderLayout.CENTER
      )

      form.add(
        imagePanel
      )

      val create =
        new JButton(
          "CREATE ACCOUNT"
        )

      form.add(
        new JLabel(
          ""
        )
      )

      form.add(
        create
      )


      choose.addActionListener(
        new ActionListener {

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

            val chooser =
              new JFileChooser()

            if (
              chooser.showOpenDialog(
                dialog
              ) ==
                JFileChooser.APPROVE_OPTION
            ) {

              val encoded =
                Util.fileToBase64(
                  chooser
                    .getSelectedFile
                    .getAbsolutePath
                )

              if (
                encoded.nonEmpty
              ) {

                avatar =
                  encoded

                imageLabel.setText(
                  "Image selected"
                )

              } else {

                JOptionPane.showMessageDialog(
                  dialog,
                  "Image invalid or larger than about 350 KB.",
                  APP_NAME,
                  JOptionPane.WARNING_MESSAGE
                )
              }
            }
          }
        }
      )


      paste.addActionListener(
        new ActionListener {

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

            val encoded =
              Util.clipboardImageBase64()

            if (
              encoded.nonEmpty
            ) {

              avatar =
                encoded

              imageLabel.setText(
                "Pasted image saved"
              )

            } else {

              JOptionPane.showMessageDialog(
                dialog,
                "Clipboard does not contain an image.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )
            }
          }
        }
      )


      create.addActionListener(
        new ActionListener {

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

            val name =
              username
                .getText
                .trim

            val pass =
              new String(
                password
                  .getPassword
              )

            val confirmPass =
              new String(
                confirm
                  .getPassword
              )

            if (
              !Util.validUsername(
                name
              )
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Username must be 3-20 characters.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }

            if (
              pass.length < 4
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Password needs at least 4 characters.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }

            if (
              pass != confirmPass
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Passwords do not match.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }

            if (
              !connect()
            ) {
              return
            }

            client.registerUsername =
              name

            client.registerPassword =
              pass

            client.registerBio =
              bio.getText.trim

            client.registerAvatar =
              avatar

            client.callback =
              registerEvent

            client.send(
              "REGISTER_START|" +
                Util.encode(
                  name
                )
            )

            dialog.dispose()
          }
        }
      )

      dialog.setContentPane(
        form
      )

      dialog.setVisible(
        true
      )
    }


    // ========================================================
    // REGISTER EVENT
    // ========================================================

    private def registerEvent(
        line: String
    ): Unit = {

      if (
        line.startsWith(
          "LOGIN_OK|"
        )
      ) {

        openMain()
      }
    }


    // ========================================================
    // FORGOT PASSWORD
    // ========================================================

    private def forgotPassword(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "FORGOT PASSWORD",
          true
        )

      dialog.setSize(
        570,
        420
      )

      dialog.setLocationRelativeTo(
        frame
      )

      val username =
        new JTextField()

      val recovery =
        new JPasswordField()

      val newPassword =
        new JPasswordField()

      val confirm =
        new JPasswordField()

      val reset =
        new JButton(
          "RESET PASSWORD"
        )

      val note =
        new JLabel(
          "<html>" +
            "Enter your Username / ID, private recovery secret,<br>" +
            "and choose a new password." +
            "</html>"
        )

      val form =
        new JPanel(
          new GridLayout(
            5,
            2,
            8,
            8
          )
        )

      form.setBorder(
        BorderFactory.createEmptyBorder(
          15,
          15,
          15,
          15
        )
      )

      form.add(
        new JLabel(
          "Info:"
        )
      )

      form.add(
        note
      )

      form.add(
        new JLabel(
          "Username / ID:"
        )
      )

      form.add(
        username
      )

      form.add(
        new JLabel(
          "Recovery Secret:"
        )
      )

      form.add(
        recovery
      )

      form.add(
        new JLabel(
          "New Password:"
        )
      )

      form.add(
        newPassword
      )

      form.add(
        new JLabel(
          "Confirm:"
        )
      )

      form.add(
        confirm
      )

      val panel =
        new JPanel(
          new BorderLayout(
            8,
            8
          )
        )

      panel.add(
        form,
        BorderLayout.CENTER
      )

      panel.add(
        reset,
        BorderLayout.SOUTH
      )

      reset.addActionListener(
        new ActionListener {

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

            val name =
              username
                .getText
                .trim

            val secret =
              new String(
                recovery
                  .getPassword
              )

            val newPass =
              new String(
                newPassword
                  .getPassword
              )

            val confirmPass =
              new String(
                confirm
                  .getPassword
              )

            if (
              !Util.validUsername(
                name
              )
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Enter valid username.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }

            if (
              secret.isEmpty
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "Enter recovery secret.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }

            if (
              newPass.length < 4
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "New password needs at least 4 characters.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }

            if (
              newPass != confirmPass
            ) {

              JOptionPane.showMessageDialog(
                dialog,
                "New passwords do not match.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )

              return
            }

            if (
              !connect()
            ) {
              return
            }

            client.recoveryUsername =
              name

            client.recoverySecret =
              secret

            client.recoveryNewPassword =
              newPass

            client.callback =
              recoveryEvent

            client.send(
              "RECOVERY_START|" +
                Util.encode(
                  name
                )
            )

            dialog.dispose()
          }
        }
      )

      dialog.setContentPane(
        panel
      )

      dialog.setVisible(
        true
      )
    }


    // ========================================================
    // RECOVERY EVENT
    // ========================================================

    private def recoveryEvent(
        line: String
    ): Unit = {

      if (
        line ==
          "RECOVERY_OK"
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Password reset successfully.\nYou can now login.",
          APP_NAME,
          JOptionPane.INFORMATION_MESSAGE
        )

        client.callback =
          loginEvent
      }
    }


    // ========================================================
    // OPEN MAIN
    // ========================================================

    private def openMain(): Unit = {

      frame.dispose()

      val main =
        new MainFrame(
          client
        )

      client.callback =
        main.handleServerEvent

      main.show()

      client.send(
        "PROFILE|" +
          Util.encode(
            client.username
          )
      )

      client.send(
        "SEARCH|" +
          Util.encode(
            ""
          )
      )
    }
  }


  // ==========================================================
  // MAIN CHAT WINDOW
  // ==========================================================

  class MainFrame(
      client: ChatClient
  ) {

    private val frame =
      new JFrame(
        APP_NAME +
          " - " +
          client.username
      )


    private val peopleModel =
      new DefaultListModel[
        PersonItem
      ]()


    private val friendsModel =
      new DefaultListModel[
        PersonItem
      ]()


    private val requestModel =
      new DefaultListModel[
        PersonItem
      ]()


    private val chatModel =
      new DefaultListModel[
        ChatItem
      ]()


    private val peopleList =
      new JList[
        PersonItem
      ](
        peopleModel
      )


    private val friendsList =
      new JList[
        PersonItem
      ](
        friendsModel
      )


    private val chatList =
      new JList[
        ChatItem
      ](
        chatModel
      )


    private val searchField =
      new JTextField()


    private val chatArea =
      new JTextArea()


    private val messageField =
      new JTextField()


    private val sendButton =
      new JButton(
        "SEND"
      )

    private val exportButton =
      new JButton(
        "EXPORT CHAT"
      )

    private val muteButton =
      new JButton(
        "MUTE CHAT"
      )

    private val logoutButton =
      new JButton(
        "LOGOUT"
      )

    private val blockButton =
      new JButton(
        "BLOCK USER"
      )


    private val chatTitle =
      new JLabel(
        "Select a chat"
      )


    private val profileName =
      new JLabel(
        "No person selected"
      )


    private val profileStatus =
      new JLabel(
        ""
      )

    private val typingLabel =
      new JLabel(
        ""
      )

    private val onlineSummary =
      new JLabel(
        "  LIVE"
      )


    private val profileBio =
      new JTextArea()


    private val profileImage =
      new JLabel(
        "No Photo",
        SwingConstants.CENTER
      )


    private val openChatButton =
      new JButton(
        "OPEN FRIEND CHAT"
      )


    private val friendButton =
      new JButton(
        "SEND FRIEND REQUEST"
      )


    private val acceptButton =
      new JButton(
        "ACCEPT REQUEST"
      )


    private val rejectButton =
      new JButton(
        "REJECT REQUEST"
      )


    private val removeFriendButton =
      new JButton(
        "REMOVE FRIEND"
      )


    private val friendsButton =
      new JButton(
        "REFRESH FRIENDS"
      )


    private val requestsButton =
      new JButton(
        "FRIEND REQUESTS"
      )


    private val groupButton =
      new JButton(
        "CREATE GROUP"
      )


    private val editButton =
      new JButton(
        "EDIT MY PROFILE"
      )


    private var selectedPerson =
      ""


    private var currentChat =
      ""


    private val chatMap =
      mutable.Map[
        String,
        ChatItem
      ]()

    private val unreadMap =
      mutable.Map[String, Int]()

    private val mutedChats =
      mutable.Set[String]()

    private val typingTimer =
      new javax.swing.Timer(900, new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = {
          if (currentChat != null && currentChat.nonEmpty) {
            client.send("TYPING|" + Util.encode(currentChat) + "|0")
          }
        }
      })


    // ========================================================
    // SHOW
    // ========================================================

    def show(): Unit = {

      Util.setupFont()

      updateFriendButtons("NONE")


      frame.setDefaultCloseOperation(
        WindowConstants.EXIT_ON_CLOSE
      )


      frame.setSize(
        1250,
        760
      )


      frame.setMinimumSize(
        new Dimension(
          1000,
          650
        )
      )


      frame.setLocationRelativeTo(
        null
      )


      val root =
        new JPanel(
          new BorderLayout(
            7,
            7
          )
        )


      // ======================================================
      // TOP BAR
      // ======================================================

      val top =
        new JPanel(
          new BorderLayout()
        )


      top.setBackground(
        new Color(
          33,
          38,
          46
        )
      )


      val title =
        new JLabel(
          "   YADNESH LIVE CHAT"
        )


      title.setForeground(
        Color.WHITE
      )


      title.setFont(
        new Font(
          "Segoe UI",
          Font.BOLD,
          22
        )
      )


      val userText =
        new JLabel(
          "Logged in: " +
            client.username +
            "   "
        )


      userText.setForeground(
        Color.WHITE
      )


      top.add(
        title,
        BorderLayout.WEST
      )


      val topRight =
        new JPanel(
          new BorderLayout(8, 0)
        )

      topRight.setOpaque(false)
      topRight.add(onlineSummary, BorderLayout.CENTER)
      topRight.add(userText, BorderLayout.WEST)
      topRight.add(logoutButton, BorderLayout.EAST)

      onlineSummary.setForeground(Color.WHITE)
      logoutButton.setFocusable(false)

      top.add(
        topRight,
        BorderLayout.EAST
      )


      root.add(
        top,
        BorderLayout.NORTH
      )


      // ======================================================
      // LEFT
      // ======================================================

      val left =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      left.setPreferredSize(
        new Dimension(
          300,
          100
        )
      )


      val searchPanel =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      val searchButton =
        new JButton(
          "SEARCH"
        )


      searchPanel.add(
        searchField,
        BorderLayout.CENTER
      )


      searchPanel.add(
        searchButton,
        BorderLayout.EAST
      )


      left.add(
        searchPanel,
        BorderLayout.NORTH
      )


      // ------------------------------------------------------
      // PEOPLE
      // ------------------------------------------------------

      peopleList.setSelectionMode(
        ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
      )


      val peoplePanel =
        new JPanel(
          new BorderLayout(
            3,
            3
          )
        )


      peoplePanel.add(
        new JLabel(
          " PEOPLE"
        ),
        BorderLayout.NORTH
      )


      peoplePanel.add(
        new JScrollPane(
          peopleList
        ),
        BorderLayout.CENTER
      )


      // ------------------------------------------------------
      // CHATS
      // ------------------------------------------------------

      val friendsPanel =
        new JPanel(
          new BorderLayout(
            3,
            3
          )
        )

      friendsPanel.add(
        new JLabel(
          " FRIENDS"
        ),
        BorderLayout.NORTH
      )

      friendsPanel.add(
        new JScrollPane(
          friendsList
        ),
        BorderLayout.CENTER
      )


      val chatPanel =
        new JPanel(
          new BorderLayout(
            3,
            3
          )
        )


      chatPanel.add(
        new JLabel(
          " MY CHATS"
        ),
        BorderLayout.NORTH
      )


      chatPanel.add(
        new JScrollPane(
          chatList
        ),
        BorderLayout.CENTER
      )


      val leftCenter =
        new JPanel(
          new GridLayout(
            3,
            1,
            5,
            5
          )
        )


      leftCenter.add(
        peoplePanel
      )


      leftCenter.add(
        friendsPanel
      )


      leftCenter.add(
        chatPanel
      )


      left.add(
        leftCenter,
        BorderLayout.CENTER
      )


      val leftActions =
        new JPanel(
          new GridLayout(
            1,
            3,
            4,
            4
          )
        )

      leftActions.add(
        groupButton
      )

      leftActions.add(
        friendsButton
      )

      leftActions.add(
        requestsButton
      )

      left.add(
        leftActions,
        BorderLayout.SOUTH
      )


      root.add(
        left,
        BorderLayout.WEST
      )


      // ======================================================
      // CENTER
      // ======================================================

      val center =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      chatTitle.setFont(
        new Font(
          "Segoe UI",
          Font.BOLD,
          18
        )
      )


      val chatHeader =
        new JPanel(
          new BorderLayout(5, 2)
        )

      chatHeader.add(
        chatTitle,
        BorderLayout.WEST
      )

      chatHeader.add(
        typingLabel,
        BorderLayout.CENTER
      )

      center.add(
        chatHeader,
        BorderLayout.NORTH
      )


      chatArea.setEditable(
        false
      )


      chatArea.setLineWrap(
        true
      )


      chatArea.setWrapStyleWord(
        true
      )


      chatArea.setFont(
        new Font(
          "Segoe UI",
          Font.PLAIN,
          14
        )
      )


      val messageScroll =
        new JScrollPane(
          chatArea
        )


      center.add(
        messageScroll,
        BorderLayout.CENTER
      )


      val sendPanel =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      sendPanel.add(
        messageField,
        BorderLayout.CENTER
      )


      val sendTools =
        new JPanel(
          new GridLayout(1, 3, 4, 4)
        )

      sendTools.add(sendButton)
      sendTools.add(exportButton)
      sendTools.add(muteButton)

      sendPanel.add(
        sendTools,
        BorderLayout.EAST
      )

      center.add(
        sendPanel,
        BorderLayout.SOUTH
      )


      root.add(
        center,
        BorderLayout.CENTER
      )


      // ======================================================
      // RIGHT PROFILE
      // ======================================================

      val right =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      right.setPreferredSize(
        new Dimension(
          285,
          100
        )
      )


      right.add(
        new JLabel(
          " PROFILE"
        ),
        BorderLayout.NORTH
      )


      profileImage.setPreferredSize(
        new Dimension(
          235,
          180
        )
      )


      profileImage.setBorder(
        BorderFactory.createLineBorder(
          new Color(
            180,
            180,
            180
          )
        )
      )


      val profileText =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      val names =
        new JPanel(
          new GridLayout(
            2,
            1,
            3,
            3
          )
        )


      names.add(
        profileName
      )


      names.add(
        profileStatus
      )


      profileText.add(
        names,
        BorderLayout.NORTH
      )


      profileBio.setEditable(
        false
      )


      profileBio.setLineWrap(
        true
      )


      profileBio.setWrapStyleWord(
        true
      )


      profileText.add(
        new JScrollPane(
          profileBio
        ),
        BorderLayout.CENTER
      )


      val profileCenter =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      profileCenter.add(
        profileImage,
        BorderLayout.NORTH
      )


      profileCenter.add(
        profileText,
        BorderLayout.CENTER
      )


      right.add(
        profileCenter,
        BorderLayout.CENTER
      )


      val profileButtons =
        new JPanel(
          new GridLayout(
            7,
            1,
            5,
            5
          )
        )

      profileButtons.add(openChatButton)
      profileButtons.add(friendButton)
      profileButtons.add(acceptButton)
      profileButtons.add(rejectButton)
      profileButtons.add(removeFriendButton)
      profileButtons.add(blockButton)
      profileButtons.add(editButton)


      right.add(
        profileButtons,
        BorderLayout.SOUTH
      )


      root.add(
        right,
        BorderLayout.EAST
      )


      // ======================================================
      // SEARCH
      // ======================================================

      searchButton.addActionListener(
        new ActionListener {

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

            searchPeople()
          }
        }
      )


      searchField.addActionListener(
        new ActionListener {

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

            searchPeople()
          }
        }
      )


      // ======================================================
      // PERSON SELECT
      // ======================================================

      peopleList.addListSelectionListener(
        _ => {

          val person =
            peopleList
              .getSelectedValue


          if (
            person != null
          ) {

            selectedPerson =
              person.username


            profileName.setText(
              person.username
            )


            profileStatus.setText(
              if (person.online) {
                "? ONLINE"
              } else {
                "? OFFLINE"
              }
            )


            profileBio.setText(
              if (
                person.bio.isEmpty
              ) {
                "No public bio."
              } else {
                person.bio
              }
            )


            client.send(
              "PROFILE|" +
                Util.encode(
                  person.username
                )
            )
          }
        }
      )


      // ======================================================
      // DOUBLE CLICK
      // ======================================================

      peopleList.addMouseListener(
        new MouseAdapter {

          override def mouseClicked(
              e: MouseEvent
          ): Unit = {

            if (
              e.getClickCount == 2
            ) {

              val person =
                peopleList
                  .getSelectedValue


              if (
                person != null
              ) {

                openDirectChat(
                  person.username
                )
              }
            }
          }
        }
      )


      // ======================================================
      // FRIEND ACTIONS
      // ======================================================

      friendButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_REQUEST|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

      acceptButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_ACCEPT|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

      rejectButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_REJECT|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

      removeFriendButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              client.send(
                "FRIEND_REMOVE|" + Util.encode(selectedPerson)
              )
            }
          }
        }
      )

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

      requestsButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            client.send("FRIEND_REQUESTS")
          }
        }
      )

      friendsList.addListSelectionListener(
        _ => {
          val person = friendsList.getSelectedValue
          if (person != null) {
            selectedPerson = person.username
            profileName.setText(person.username)
            profileStatus.setText(if (person.online) "? ONLINE" else "? OFFLINE")
            profileBio.setText(if (person.bio.isEmpty) "No public bio." else person.bio)
            client.send("PROFILE|" + Util.encode(person.username))
          }
        }
      )

      friendsList.addMouseListener(
        new MouseAdapter {
          override def mouseClicked(e: MouseEvent): Unit = {
            if (e.getClickCount == 2) {
              val person = friendsList.getSelectedValue
              if (person != null) openDirectChat(person.username)
            }
          }
        }
      )


      // ======================================================
      // OPEN DIRECT CHAT
      // ======================================================

      openChatButton.addActionListener(
        new ActionListener {

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

            if (
              selectedPerson != null &&
              selectedPerson.nonEmpty
            ) {

              openDirectChat(
                selectedPerson
              )
            }
          }
        }
      )


      // ======================================================
      // CHAT LIST
      // ======================================================

      chatList.addListSelectionListener(
        _ => {

          val chat =
            chatList
              .getSelectedValue


          if (
            chat != null
          ) {

            currentChat =
              chat.id

            unreadMap.remove(chat.id)
            val cleared = chat.copy(unread = 0)
            chatMap.put(chat.id, cleared)
            replaceChatItem(cleared)
            typingLabel.setText("")
            typingTimer.stop()
            client.send("TYPING|" + Util.encode(chat.id) + "|0")
            muteButton.setText(if (mutedChats.contains(chat.id)) "UNMUTE CHAT" else "MUTE CHAT")

            chatTitle.setText(
              chat.title
            )


            chatArea.setText(
              "Loading messages..."
            )


            client.send(
              "OPEN_CHAT|" +
                Util.encode(
                  chat.id
                )
            )
          }
        }
      )


      // ======================================================
      // SEND
      // ======================================================

      sendButton.addActionListener(
        new ActionListener {

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

            sendMessage()
          }
        }
      )


      messageField.addActionListener(
        new ActionListener {

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

            sendMessage()
          }
        }
      )


      // ======================================================
      // EDIT
      // ======================================================

      editButton.addActionListener(
        new ActionListener {

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

            editProfile()
          }
        }
      )

      blockButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (selectedPerson.nonEmpty) {
              if (blockButton.getText == "UNBLOCK USER") {
                client.send("UNBLOCK_USER|" + Util.encode(selectedPerson))
              } else {
                val ok = JOptionPane.showConfirmDialog(
                  frame,
                  "Block " + selectedPerson + "? Friend requests and direct chat will be stopped.",
                  APP_NAME,
                  JOptionPane.YES_NO_OPTION,
                  JOptionPane.WARNING_MESSAGE
                )
                if (ok == JOptionPane.YES_OPTION) {
                  client.send("BLOCK_USER|" + Util.encode(selectedPerson))
                }
              }
            }
          }
        }
      )

      logoutButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            val ok = JOptionPane.showConfirmDialog(
              frame,
              "Logout from " + client.username + "?",
              APP_NAME,
              JOptionPane.YES_NO_OPTION
            )
            if (ok == JOptionPane.YES_OPTION) {
              client.send("LOGOUT")
              client.close()
              frame.dispose()
              new LoginFrame().show()
            }
          }
        }
      )

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

      muteButton.addActionListener(
        new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            if (currentChat != null && currentChat.nonEmpty) {
              if (mutedChats.contains(currentChat)) {
                mutedChats.remove(currentChat)
                muteButton.setText("MUTE CHAT")
              } else {
                mutedChats.add(currentChat)
                muteButton.setText("UNMUTE CHAT")
              }
            }
          }
        }
      )

      messageField.getDocument.addDocumentListener(
        new DocumentListener {
          private def changed(): Unit = {
            if (currentChat != null && currentChat.nonEmpty && messageField.getText.nonEmpty) {
              typingTimer.restart()
              client.send(
                "TYPING|" + Util.encode(currentChat) + "|1"
              )
            }
          }
          override def insertUpdate(e: DocumentEvent): Unit = changed()
          override def removeUpdate(e: DocumentEvent): Unit = changed()
          override def changedUpdate(e: DocumentEvent): Unit = changed()
        }
      )


      frame.setContentPane(
        root
      )


      frame.setVisible(
        true
      )

      refreshFriends()
      client.send("FRIEND_REQUESTS")
      searchPeople()
    }


    private def replaceChatItem(item: ChatItem): Unit = {
      chatMap.put(item.id, item)
      var i = 0
      while (i < chatModel.size()) {
        val old = chatModel.getElementAt(i)
        if (old.id == item.id) {
          chatModel.setElementAt(item, i)
          return
        }
        i += 1
      }
      chatModel.addElement(item)
    }


    // ========================================================
    // SEARCH
    // ========================================================

    private def searchPeople(): Unit = {

      client.send(
        "SEARCH|" +
          Util.encode(
            searchField
              .getText
              .trim
          )
      )
    }


    // ========================================================
    // REFRESH FRIENDS
    // ========================================================

    private def refreshFriends(): Unit = {
      client.send(
        "FRIENDS_LIST"
      )
    }


    // ========================================================
    // DIRECT CHAT
    // ========================================================

    private def openDirectChat(
        username: String
    ): Unit = {

      client.send(
        "OPEN_DM|" +
          Util.encode(
            username
          )
      )
    }


    // ========================================================
    // SEND MESSAGE
    // ========================================================

    private def sendMessage(): Unit = {

      if (
        currentChat == null ||
        currentChat.isEmpty
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "First select a chat.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      val message =
        messageField
          .getText
          .trim


      if (
        message.isEmpty
      ) {

        return
      }


      typingTimer.stop()
      client.send("TYPING|" + Util.encode(currentChat) + "|0")

      client.send(
        "SEND_MESSAGE|" +
          Util.encode(
            currentChat
          ) +
          "|" +
          Util.encode(
            message
          )
      )


      messageField.setText(
        ""
      )
    }


    private def exportCurrentChat(): Unit = {
      if (currentChat == null || currentChat.isEmpty) {
        JOptionPane.showMessageDialog(
          frame,
          "First open a chat.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )
        return
      }

      val chooser = new JFileChooser()
      chooser.setDialogTitle("EXPORT CHAT")
      chooser.setSelectedFile(new java.io.File("YadneshChat_" + System.currentTimeMillis() + ".txt"))

      if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
        try {
          Files.write(
            chooser.getSelectedFile.toPath,
            chatArea.getText.getBytes(StandardCharsets.UTF_8)
          )
          JOptionPane.showMessageDialog(
            frame,
            "Chat exported successfully.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )
        } catch {
          case _: Throwable =>
            JOptionPane.showMessageDialog(
              frame,
              "Could not export chat.",
              APP_NAME,
              JOptionPane.ERROR_MESSAGE
            )
        }
      }
    }


    // ========================================================
    // CREATE GROUP
    // ========================================================

    private def createGroup(): Unit = {

      val selected =
        peopleList
          .getSelectedValuesList()


      if (
        selected == null ||
        selected.size() < 1
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "Select at least one person.",
          APP_NAME,
          JOptionPane.WARNING_MESSAGE
        )

        return
      }


      val name =
        JOptionPane.showInputDialog(
          frame,
          "Group name:",
          "CREATE GROUP",
          JOptionPane.QUESTION_MESSAGE
        )


      if (
        name == null ||
        name.trim.isEmpty
      ) {

        return
      }


      val request =
        new StringBuilder()


      request.append(
        "CREATE_GROUP|"
      )


      request.append(
        Util.encode(
          name.trim
        )
      )


      request.append(
        "|" +
          selected.size()
      )


      var i =
        0


      while (
        i < selected.size()
      ) {

        val person =
          selected.get(
            i
          )


        request.append(
          "|" +
            Util.encode(
              person.username
            )
        )


        i +=
          1
      }


      client.send(
        request.toString
      )
    }


    // ========================================================
    // EDIT PROFILE
    // ========================================================

    private def editProfile(): Unit = {

      val dialog =
        new JDialog(
          frame,
          "EDIT MY PROFILE",
          true
        )


      dialog.setSize(
        700,
        540
      )


      dialog.setLocationRelativeTo(
        frame
      )


      val bio =
        new JTextField(
          client.ownBio
        )


      val choose =
        new JButton(
          "CHOOSE IMAGE"
        )


      val paste =
        new JButton(
          "PASTE IMAGE"
        )


      val imageLabel =
        new JLabel(
          if (
            client.ownAvatar.nonEmpty
          ) {
            "Current image saved ?"
          } else {
            "No image"
          }
        )


      var avatar =
        client.ownAvatar


      val save =
        new JButton(
          "SAVE PROFILE"
        )


      val form =
        new JPanel(
          new GridLayout(
            4,
            2,
            8,
            8
          )
        )


      form.setBorder(
        BorderFactory.createEmptyBorder(
          15,
          15,
          15,
          15
        )
      )


      form.add(
        new JLabel(
          "Username:"
        )
      )


      form.add(
        new JLabel(
          client.username
        )
      )


      form.add(
        new JLabel(
          "Public Bio:"
        )
      )


      form.add(
        bio
      )


      form.add(
        new JLabel(
          "Profile Image:"
        )
      )


      val imagePanel =
        new JPanel(
          new BorderLayout(
            5,
            5
          )
        )


      val imageButtons =
        new JPanel(
          new GridLayout(
            1,
            2,
            5,
            5
          )
        )


      imageButtons.add(
        choose
      )


      imageButtons.add(
        paste
      )


      imagePanel.add(
        imageButtons,
        BorderLayout.WEST
      )


      imagePanel.add(
        imageLabel,
        BorderLayout.CENTER
      )


      form.add(
        imagePanel
      )


      form.add(
        new JLabel(
          "Save:"
        )
      )


      form.add(
        save
      )


      choose.addActionListener(
        new ActionListener {

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

            val chooser =
              new JFileChooser()


            if (
              chooser.showOpenDialog(
                dialog
              ) ==
                JFileChooser.APPROVE_OPTION
            ) {

              val encoded =
                Util.fileToBase64(
                  chooser
                    .getSelectedFile
                    .getAbsolutePath
                )


              if (
                encoded.nonEmpty
              ) {

                avatar =
                  encoded


                imageLabel.setText(
                  "New image selected ?"
                )

              } else {

                JOptionPane.showMessageDialog(
                  dialog,
                  "Image invalid or too large.",
                  APP_NAME,
                  JOptionPane.WARNING_MESSAGE
                )
              }
            }
          }
        }
      )


      paste.addActionListener(
        new ActionListener {

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

            val encoded =
              Util.clipboardImageBase64()


            if (
              encoded.nonEmpty
            ) {

              avatar =
                encoded


              imageLabel.setText(
                "Pasted image saved ?"
              )

            } else {

              JOptionPane.showMessageDialog(
                dialog,
                "Clipboard does not contain an image.",
                APP_NAME,
                JOptionPane.WARNING_MESSAGE
              )
            }
          }
        }
      )


      save.addActionListener(
        new ActionListener {

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

            client.ownBio =
              bio.getText.trim


            client.ownAvatar =
              avatar


            client.send(
              "UPDATE_PROFILE|" +
                Util.encode(
                  client.ownBio
                ) +
                "|" +
                Util.encode(
                  client.ownAvatar
                )
            )


            dialog.dispose()
          }
        }
      )


      dialog.setContentPane(
        form
      )


      dialog.setVisible(
        true
      )
    }


    // ========================================================
    // SERVER EVENTS
    // ========================================================

    def handleServerEvent(
        line: String
    ): Unit = {

      val p =
        line.split(
          "\\|",
          -1
        )


      if (
        p.length == 0
      ) {
        return
      }


      p(0) match {

        // ----------------------------------------------------
        // SEARCH RESULTS
        // ----------------------------------------------------

        case "SEARCH_RESULT" =>

          peopleModel.clear()


          if (
            p.length >= 2
          ) {

            val count =
              try {
                p(1).toInt
              } catch {
                case _: Throwable =>
                  0
              }


            var index =
              2


            var i =
              0


            while (
              i < count &&
              index + 2 <
                p.length
            ) {

              val name =
                Util.decode(
                  p(index)
                )


              val bio =
                Util.decode(
                  p(index + 1)
                )


              val online =
                p(index + 2) ==
                  "1"

              val relation =
                if (index + 3 < p.length) p(index + 3) else "NONE"

              peopleModel.addElement(
                PersonItem(
                  name,
                  bio,
                  online,
                  relation
                )
              )


              index +=
                (if (index + 3 < p.length) 4 else 3)


              i +=
                1
            }
          }


        // ----------------------------------------------------
        // PROFILE
        // ----------------------------------------------------

        case "PROFILE_DATA" =>

          if (
            p.length >= 5
          ) {

            val name =
              Util.decode(
                p(1)
              )


            val bio =
              Util.decode(
                p(2)
              )


            val avatar =
              Util.decode(
                p(3)
              )


            val online =
              p(4) ==
                "1"

            val relation =
              if (p.length >= 6) p(5) else "NONE"

            updateFriendButtons(relation)


            if (
              name.equalsIgnoreCase(
                client.username
              )
            ) {

              client.ownBio =
                bio


              client.ownAvatar =
                avatar
            }


            if (
              name.equalsIgnoreCase(
                selectedPerson
              )
            ) {

              profileName.setText(
                name
              )


              profileStatus.setText(
                if (online) {
                  "? ONLINE"
                } else {
                  "? OFFLINE"
                }
              )


              profileBio.setText(
                if (bio.isEmpty) {
                  "No public bio."
                } else {
                  bio
                }
              )


              displayAvatar(
                avatar
              )
            }
          }


        // ----------------------------------------------------
        // PROFILE SAVED
        // ----------------------------------------------------

        case "PROFILE_SAVED" =>

          JOptionPane.showMessageDialog(
            frame,
            "Profile saved permanently on the host server.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )


          client.send(
            "PROFILE|" +
              Util.encode(
                client.username
              )
          )


          searchPeople()


        // ----------------------------------------------------
        // PROFILE CHANGED
        // ----------------------------------------------------

        case "PROFILE_CHANGED" =>

          if (
            p.length >= 2
          ) {

            val name =
              Util.decode(
                p(1)
              )


            if (
              name.equalsIgnoreCase(
                selectedPerson
              )
            ) {

              client.send(
                "PROFILE|" +
                  Util.encode(
                    selectedPerson
                  )
              )
            }


            searchPeople()
            refreshFriends()
          }


        // ----------------------------------------------------
        // CHAT
        // ----------------------------------------------------

        case "CHAT" =>

          if (
            p.length >= 4
          ) {

            val id =
              Util.decode(
                p(1)
              )


            val title =
              Util.decode(
                p(2)
              )


            val group =
              p(3) ==
                "1"


            if (
              !chatMap.contains(
                id
              )
            ) {

              val entry =
                ChatItem(
                  id,
                  title,
                  group,
                  unreadMap.getOrElse(id, 0)
                )


              chatMap.put(
                id,
                entry
              )


              chatModel.addElement(
                entry
              )
            }
          }


        // ----------------------------------------------------
        // HISTORY
        // ----------------------------------------------------

        case "HISTORY" =>

          if (
            p.length >= 3
          ) {

            val id =
              Util.decode(
                p(1)
              )


            val count =
              try {
                p(2).toInt
              } catch {
                case _: Throwable =>
                  0
              }


            if (
              id ==
                currentChat
            ) {

              val out =
                new StringBuilder()


              var index =
                3


              var i =
                0


              while (
                i < count &&
                index + 2 <
                  p.length
              ) {

                val sender =
                  Util.decode(
                    p(index)
                  )


                val time =
                  Util.decode(
                    p(index + 1)
                  )


                val message =
                  Util.decode(
                    p(index + 2)
                  )


                out.append(
                  "[" +
                    time +
                    "] " +
                    sender +
                    ": " +
                    message +
                    "\n"
                )


                index +=
                  3


                i +=
                  1
              }


              chatArea.setText(
                out.toString
              )


              chatArea.setCaretPosition(
                chatArea.getDocument
                  .getLength
              )
            }
          }


        // ----------------------------------------------------
        // LIVE MESSAGE
        // ----------------------------------------------------

        case "MESSAGE" =>

          if (
            p.length >= 5
          ) {

            val id =
              Util.decode(
                p(1)
              )


            val sender =
              Util.decode(
                p(2)
              )


            val time =
              Util.decode(
                p(3)
              )


            val message =
              Util.decode(
                p(4)
              )


            if (
              id ==
                currentChat
            ) {

              typingLabel.setText("")
              chatArea.append(
                "[" +
                  time +
                  "] " +
                  sender +
                  ": " +
                  message +
                  "\n"
              )


              chatArea.setCaretPosition(
                chatArea.getDocument
                  .getLength
              )
            } else {
              val next = unreadMap.getOrElse(id, 0) + 1
              unreadMap.put(id, next)
              chatMap.get(id).foreach { old =>
                replaceChatItem(old.copy(unread = next))
              }
              if (!mutedChats.contains(id)) {
                Toolkit.getDefaultToolkit.beep()
              }
            }
          }


        // ----------------------------------------------------
        // TYPING
        // ----------------------------------------------------

        case "TYPING" =>

          if (p.length >= 4) {
            val id = Util.decode(p(1))
            val name = Util.decode(p(2))
            val state = p(3) == "1"
            if (id == currentChat && !name.equalsIgnoreCase(client.username)) {
              typingLabel.setText(if (state) name + " is typing..." else "")
            }
          }


        // ----------------------------------------------------
        // PRESENCE
        // ----------------------------------------------------

        case "PRESENCE" =>

          if (
            p.length >= 3
          ) {

            val name =
              Util.decode(
                p(1)
              )


            val online =
              p(2) ==
                "1"


            var i =
              0


            while (
              i < peopleModel.size()
            ) {

              val old =
                peopleModel
                  .getElementAt(
                    i
                  )


              if (
                old.username
                  .equalsIgnoreCase(
                    name
                  )
              ) {

                peopleModel.setElementAt(
                  old.copy(
                    online =
                      online
                  ),
                  i
                )
              }


              i +=
                1
            }


            var j = 0
            while (j < friendsModel.size()) {
              val oldFriend = friendsModel.getElementAt(j)
              if (oldFriend.username.equalsIgnoreCase(name)) {
                friendsModel.setElementAt(oldFriend.copy(online = online), j)
              }
              j += 1
            }

            if (
              selectedPerson
                .equalsIgnoreCase(
                  name
                )
            ) {

              profileStatus.setText(
                if (online) {
                  "? ONLINE"
                } else {
                  "? OFFLINE"
                }
              )
            }
          }


        // ----------------------------------------------------
        // FRIENDS LIST
        // ----------------------------------------------------

        case "FRIENDS_LIST" =>

          friendsModel.clear()

          if (p.length >= 2) {
            val count = try p(1).toInt catch { case _: Throwable => 0 }
            var onlineCount = 0
            var index = 2
            var i = 0
            while (i < count && index + 2 < p.length) {
              val name = Util.decode(p(index))
              val online = p(index + 1) == "1"
              if (online) onlineCount += 1
              val bio = Util.decode(p(index + 2))
              friendsModel.addElement(PersonItem(name, bio, online, "FRIEND"))
              index += 3
              i += 1
            }
            onlineSummary.setText("  FRIENDS ONLINE: " + onlineCount + "/" + count + "  ")
          }

        // ----------------------------------------------------
        // FRIEND REQUESTS
        // ----------------------------------------------------

        case "FRIEND_REQUESTS" =>

          requestModel.clear()

          if (p.length >= 2) {
            val count = try p(1).toInt catch { case _: Throwable => 0 }
            var index = 2
            var i = 0
            while (i < count && index + 2 < p.length) {
              val name = Util.decode(p(index))
              val online = p(index + 1) == "1"
              val bio = Util.decode(p(index + 2))
              requestModel.addElement(PersonItem(name, bio, online, "INCOMING"))
              index += 3
              i += 1
            }

            requestsButton.setText(
              if (count > 0) "FRIEND REQUESTS (" + count + ")" else "FRIEND REQUESTS"
            )

            if (count > 0) {
              val choices = new Array[String](requestModel.size())
              var k = 0
              while (k < requestModel.size()) {
                choices(k) = requestModel.getElementAt(k).username
                k += 1
              }

              val choiceObjects = choices.map(_.asInstanceOf[Object])

              val selected = JOptionPane.showInputDialog(
                frame,
                "Pending friend requests. Select a person:",
                "FRIEND REQUESTS",
                JOptionPane.QUESTION_MESSAGE,
                null,
                choiceObjects,
                choices(0)
              )

              if (selected != null) {
                val action = JOptionPane.showOptionDialog(
                  frame,
                  "Friend request from " + selected,
                  "FRIEND REQUEST",
                  JOptionPane.DEFAULT_OPTION,
                  JOptionPane.QUESTION_MESSAGE,
                  null,
                  Array[AnyRef]("ACCEPT", "REJECT", "CANCEL"),
                  "ACCEPT"
                )
                if (action == 0) {
                  client.send("FRIEND_ACCEPT|" + Util.encode(selected.toString))
                } else if (action == 1) {
                  client.send("FRIEND_REJECT|" + Util.encode(selected.toString))
                }
              }
            }
          }

        // ----------------------------------------------------
        // FRIEND EVENTS
        // ----------------------------------------------------

        case "FRIEND_REQUEST_SENT" =>
          searchPeople()

        case "FRIEND_REQUEST_RECEIVED" =>
          val from = if (p.length >= 2) Util.decode(p(1)) else "Someone"
          JOptionPane.showMessageDialog(
            frame,
            from + " sent you a friend request. Open FRIEND REQUESTS to accept or reject.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )
          client.send("FRIEND_REQUESTS")

        case "FRIEND_ACCEPTED" =>
          val accepted = if (p.length >= 2) Util.decode(p(1)) else "Friend"
          JOptionPane.showMessageDialog(
            frame,
            "You are now friends with " + accepted + ". The private chat is ready.",
            APP_NAME,
            JOptionPane.INFORMATION_MESSAGE
          )
          refreshFriends()
          searchPeople()

        case "FRIEND_REQUEST_REJECTED" =>
          refreshFriends()
          searchPeople()

        case "FRIEND_REMOVED" =>
          refreshFriends()
          searchPeople()

        case "BLOCK_STATE" =>
          val blockedName = if (p.length >= 2) Util.decode(p(1)) else "User"
          val blocked = p.length >= 3 && p(2) == "1"
          blockButton.setText(if (blocked) "UNBLOCK USER" else "BLOCK USER")
          if (blocked) {
            JOptionPane.showMessageDialog(frame, blockedName + " is blocked.", APP_NAME, JOptionPane.INFORMATION_MESSAGE)
          }
          refreshFriends()
          searchPeople()
          if (selectedPerson.equalsIgnoreCase(blockedName)) {
            client.send("PROFILE|" + Util.encode(selectedPerson))
          }

        case "BLOCKED_BY" =>
          val blocker = if (p.length >= 2) Util.decode(p(1)) else "A user"
          JOptionPane.showMessageDialog(frame, blocker + " blocked you.", APP_NAME, JOptionPane.WARNING_MESSAGE)
          refreshFriends()
          searchPeople()

        case "UNBLOCKED_BY" =>
          refreshFriends()
          searchPeople()

        case "CLIENT_DISCONNECTED" =>
          onlineSummary.setText("  CONNECTION LOST  ")
          typingLabel.setText("")
          JOptionPane.showMessageDialog(
            frame,
            "The connection to the host server was lost. Restart/login again to reconnect.",
            APP_NAME,
            JOptionPane.WARNING_MESSAGE
          )


        // ----------------------------------------------------
        // GROUP
        // ----------------------------------------------------

        case "GROUP_CREATED" =>

          if (
            p.length >= 2
          ) {

            JOptionPane.showMessageDialog(
              frame,
              "Group created:\n" +
                Util.decode(
                  p(1)
                ),
              APP_NAME,
              JOptionPane.INFORMATION_MESSAGE
            )
          }


        case _ =>
      }
    }


    // ========================================================
    // FRIEND BUTTON STATE
    // ========================================================

    private def updateFriendButtons(
        relation: String
    ): Unit = {

      friendButton.setEnabled(
        relation == "NONE"
      )

      acceptButton.setEnabled(
        relation == "INCOMING"
      )

      rejectButton.setEnabled(
        relation == "INCOMING"
      )

      removeFriendButton.setEnabled(
        relation == "FRIEND"
      )

      openChatButton.setEnabled(
        relation == "FRIEND"
      )

      blockButton.setEnabled(
        selectedPerson.nonEmpty && relation != ""
      )

      blockButton.setText(
        if (relation == "BLOCKED") "UNBLOCK USER" else "BLOCK USER"
      )
    }


    // ========================================================
    // DISPLAY AVATAR
    // ========================================================

    private def displayAvatar(
        base64: String
    ): Unit = {

      if (
        base64 == null ||
        base64.isEmpty
      ) {

        profileImage.setIcon(
          null
        )


        profileImage.setText(
          "No Photo"
        )


      } else {

        val image =
          Util.base64ToImage(
            base64
          )


        if (
          image == null
        ) {

          profileImage.setIcon(
            null
          )


          profileImage.setText(
            "No Photo"
          )


        } else {

          val scaled =
            image.getScaledInstance(
              225,
              165,
              Image.SCALE_SMOOTH
            )


          profileImage.setText(
            ""
          )


          profileImage.setIcon(
            new javax.swing.ImageIcon(
              scaled
            )
          )
        }
      }
    }
  }


  // ==========================================================
  // START SCREEN
  // ==========================================================

  object StartScreen {

    def show(): Unit = {

      Util.setupFont()


      val frame =
        new JFrame(
          APP_NAME
        )


      frame.setDefaultCloseOperation(
        WindowConstants.EXIT_ON_CLOSE
      )


      frame.setSize(
        720,
        520
      )


      frame.setLocationRelativeTo(
        null
      )


      val root =
        new JPanel(
          new BorderLayout(
            15,
            15
          )
        )


      root.setBorder(
        BorderFactory.createEmptyBorder(
          30,
          30,
          30,
          30
        )
      )


      val title =
        new JLabel(
          "<html>" +
            "<center>" +
            "<font size='7'><b>YADNESH LIVE CHAT</b></font><br>" +
            "<font size='4'>PERMANENT ACCOUNT ? WIFI LIVE CHAT</font>" +
            "</center>" +
            "</html>",
          SwingConstants.CENTER
        )


      root.add(
        title,
        BorderLayout.NORTH
      )


      val info =
        new JTextArea(
          "FEATURES\n\n" +
            "? Permanent user accounts\n" +
            "? Same account after restarting app\n" +
            "? Same account available from another PC through the same server\n" +
            "? PC to PC live Wi-Fi chat\n" +
            "? 2-person direct chat\n" +
            "? Multiple-person group chat\n" +
            "? Username search\n" +
            "? Online / offline status\n" +
            "? Profile photo\n" +
            "? CHOOSE IMAGE\n" +
            "? PASTE IMAGE FROM CLIPBOARD\n" +
            "? Public profile bio\n" +
            "? Forgot Password recovery\n" +
            "? Live typing indicator\n" +
            "? Unread message counters\n" +
            "? Block / unblock users\n" +
            "? Logout + connection heartbeat\n" +
            "? Export current chat to TXT\n\n" +
            "Account files are stored on the HOST SERVER PC."
        )


      info.setEditable(
        false
      )


      info.setLineWrap(
        true
      )


      info.setWrapStyleWord(
        true
      )


      root.add(
        new JScrollPane(
          info
        ),
        BorderLayout.CENTER
      )


      val start =
        new JButton(
          "START YADNESH LIVE CHAT"
        )


      start.setPreferredSize(
        new Dimension(
          330,
          55
        )
      )


      root.add(
        start,
        BorderLayout.SOUTH
      )


      start.addActionListener(
        new ActionListener {

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

            frame.dispose()


            val login =
              new LoginFrame()


            login.show()
          }
        }
      )


      frame.setContentPane(
        root
      )


      frame.setVisible(
        true
      )
    }
  }


  // ==========================================================
  // LAUNCHER
  // ==========================================================

  def startApp(): Unit = {

    SwingUtilities.invokeLater(
      new Runnable {

        override def run(): Unit = {

          try {

            UIManager.setLookAndFeel(
              UIManager
                .getSystemLookAndFeelClassName
            )

          } catch {

            case _: Throwable =>
          }


          StartScreen.show()
        }
      }
    )
  }
}


// ============================================================
// KOJO DIRECT RUN
// ============================================================

LiveChatApp.startApp()