Code Sketch
yoiii ch
Category: Programming
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.Rectangle
import java.awt.Robot
import java.awt.GraphicsEnvironment
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.BufferedOutputStream
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.InetAddress
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.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
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.imageio.IIOImage
import javax.imageio.ImageTypeSpecifier
import javax.imageio.ImageWriteParam
import javax.imageio.ImageWriter
import javax.imageio.stream.ImageOutputStream
import javax.imageio.metadata.IIOMetadataNode
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
private val MAX_MEDIA_BYTES =
25L * 1024L * 1024L
// Larger chunks reduce protocol/Base64 overhead during media transfer.
private val MEDIA_CHUNK_BYTES =
48 * 1024
// ZERO-INPUT INTERNET AUTO CONNECT.
// Leave empty for local/LAN automatic mode. For an Internet
// deployment, set your own public Yadnesh server hostname once
// in the developer build. Users will not see or type host/port.
private val AUTO_PUBLIC_SERVER_HOST = {
val systemHost =
System.getProperty("yadnesh.chat.server", "").trim
if (systemHost.nonEmpty) {
systemHost
} else {
System.getenv("YADNESH_CHAT_SERVER") match {
case null => ""
case value => value.trim
}
}
}
// LIVE SCREEN SHARE: host must explicitly switch sharing ON.
// The viewer UI can keep this password pre-filled and masked.
private val SCREEN_SHARE_PASSWORD = "yadnesh2013"
private val SCREEN_CAPTURE_DELAY_MS = 140L
private val SCREEN_JPEG_QUALITY = 0.48f
private val MAX_SCREEN_FRAME_BYTES = 850 * 1024
private val AUTO_PUBLIC_SERVER_PORT = {
val text =
System.getProperty("yadnesh.chat.port", DEFAULT_PORT.toString).trim
try {
val value = text.toInt
if (value >= 1 && value <= 65535) value else DEFAULT_PORT
} catch {
case _: Throwable => DEFAULT_PORT
}
}
// ==========================================================
// 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"
)
)
}
// Full date-time used by persistent message/media records.
// Kept separate from now() so existing UI timestamps remain compact.
def timeNow(): String = {
LocalDateTime.now().format(
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss"
)
)
}
// Safe file name for Windows/Linux/macOS and for received media.
// This also prevents path traversal through names such as "../file".
def safeFilename(value: String): String = {
val original =
if (value == null) "file" else value.trim
val base =
if (original.isEmpty) "file" else original
val cleaned =
base
.replaceAll("[\\/:*?\"<>|]", "_")
.replaceAll("\\\\", "_")
.replaceAll("\\.\\.", "_")
.replaceAll("[\\r\\n\\t]", "_")
.trim
val result =
if (cleaned.isEmpty || cleaned == "." || cleaned == "..") {
"file"
} else {
cleaned
}
clean(result, 180)
}
// --------------------------------------------------------
// 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 humanSize(bytes: Long): String = {
if (bytes < 1024L) bytes + " B"
else if (bytes < 1024L * 1024L) "%.1f KB".format(bytes.toDouble / 1024.0)
else "%.1f MB".format(bytes.toDouble / (1024.0 * 1024.0))
}
// --------------------------------------------------------
// OPEN MEDIA WITH THE DEFAULT SYSTEM APPLICATION
// --------------------------------------------------------
def openMedia(path: Path): Unit = {
try {
if (
path != null &&
Files.exists(path) &&
java.awt.Desktop.isDesktopSupported
) {
java.awt.Desktop.getDesktop.open(path.toFile)
}
} catch {
case _: Throwable =>
}
}
// --------------------------------------------------------
// DEPENDENCY-FREE ANIMATED GIF WRITER
// --------------------------------------------------------
class AnimatedGifWriter(
output: ImageOutputStream,
delayMs: Int
) {
private val writer: ImageWriter =
ImageIO.getImageWritersBySuffix("gif").next()
private val params: ImageWriteParam =
writer.getDefaultWriteParam
private var firstFrame =
true
writer.setOutput(output)
writer.prepareWriteSequence(null)
private def metadataFor(
image: BufferedImage
): javax.imageio.metadata.IIOMetadata = {
val metadata =
writer.getDefaultImageMetadata(
new ImageTypeSpecifier(image),
params
)
val root =
metadata
.getAsTree(
"javax_imageio_gif_image_1.0"
)
.asInstanceOf[IIOMetadataNode]
val gce =
new IIOMetadataNode(
"GraphicControlExtension"
)
gce.setAttribute(
"disposalMethod",
"none"
)
gce.setAttribute(
"userInputFlag",
"FALSE"
)
gce.setAttribute(
"transparentColorFlag",
"FALSE"
)
gce.setAttribute(
"delayTime",
math.max(2, delayMs / 10).toString
)
gce.setAttribute(
"transparentColorIndex",
"0"
)
root.appendChild(gce)
if (firstFrame) {
val apps =
new IIOMetadataNode(
"ApplicationExtensions"
)
val app =
new IIOMetadataNode(
"ApplicationExtension"
)
app.setAttribute(
"applicationID",
"NETSCAPE"
)
app.setAttribute(
"authenticationCode",
"2.0"
)
app.setUserObject(
Array[Byte](1, 0, 0)
)
apps.appendChild(app)
root.appendChild(apps)
}
metadata.setFromTree(
"javax_imageio_gif_image_1.0",
root
)
metadata
}
def writeFrame(
image: BufferedImage
): Unit = {
writer.writeToSequence(
new IIOImage(
image,
null,
metadataFor(image)
),
params
)
firstFrame =
false
}
def close(): Unit = {
try {
writer.endWriteSequence()
} catch {
case _: Throwable =>
}
try {
output.close()
} catch {
case _: Throwable =>
}
try {
writer.dispose()
} catch {
case _: Throwable =>
}
}
}
def openFile(path: Path): Unit = {
try {
if (java.awt.Desktop.isDesktopSupported) {
java.awt.Desktop.getDesktop.open(path.toFile)
}
} catch {
case _: Throwable =>
}
}
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
}
}
}
case class ReelItem(
fileId: String,
sender: String,
time: String,
fileName: String,
chatTitle: String,
size: Long
) {
override def toString: String = {
chatTitle +
" ? " +
sender +
" ? " +
fileName +
" ? " +
time
}
}
// ==========================================================
// 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 screenSharing =
mutable.Set[String]()
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)
]()
private val pendingScreenRequests =
mutable.Map[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 val groupsFolder =
dataFolder.resolve(
"groups"
)
private val mediaFolder =
dataFolder.resolve(
"media"
)
private val mediaIndexFile =
mediaFolder.resolve(
"index.txt"
)
case class MediaInfo(
fileId: String,
chatId: String,
sender: String,
time: String,
fileName: String,
kind: String,
size: Long,
storedPath: Path
)
case class MediaUpload(
chatId: String,
fileId: String,
fileName: String,
kind: String,
size: Long,
tempPath: Path,
out: java.io.OutputStream,
var written: Long,
sender: String
)
private val mediaIndex =
mutable.Map[String, MediaInfo]()
private val mediaUploads =
mutable.Map[Socket, MediaUpload]()
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 only this socket. Media traffic from one user cannot
// freeze messages for every other connected user.
this.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
)
Files.createDirectories(
groupsFolder
)
Files.createDirectories(
mediaFolder
)
loadProfiles()
loadGroups()
loadMediaIndex()
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
)
}
}
// ========================================================
// PERMANENT GROUP STORAGE
// ========================================================
private def groupFile(
groupId: String
): Path = {
groupsFolder.resolve(
Util.sha256(groupId) +
".group"
)
}
private def saveGroup(
groupId: String
): Unit = {
try {
val props =
new Properties()
props.setProperty(
"id",
groupId
)
props.setProperty(
"name",
chatNames.getOrElse(
groupId,
"Group"
)
)
val members =
chatMembers.getOrElse(
groupId,
mutable.Set[String]()
)
props.setProperty(
"count",
members.size.toString
)
var i = 0
members.foreach { member =>
props.setProperty(
"member" + i,
member
)
i += 1
}
val out =
Files.newOutputStream(
groupFile(groupId)
)
try {
props.store(
out,
"Yadnesh Live Chat Permanent Group"
)
} finally {
out.close()
}
} catch {
case _: Throwable =>
}
}
private def loadGroups(): Unit = {
chatMembers.keys
.filter(_.startsWith("GROUP:"))
.toSeq
.foreach { id =>
chatMembers.remove(id)
}
chatNames.keys
.filter(_.startsWith("GROUP:"))
.toSeq
.foreach { id =>
chatNames.remove(id)
}
chatGroups.keys
.filter(_.startsWith("GROUP:"))
.toSeq
.foreach { id =>
chatGroups.remove(id)
}
if (!Files.exists(groupsFolder)) return
val stream = Files.list(groupsFolder)
try {
val it = stream.iterator()
while (it.hasNext) {
val file = it.next()
if (file.toString.endsWith(".group")) {
try {
val props = new Properties()
val in = Files.newInputStream(file)
try {
props.load(in)
} finally {
in.close()
}
val id = props.getProperty("id", "")
val name = props.getProperty("name", "Group")
val count = try {
props.getProperty("count", "0").toInt
} catch {
case _: Throwable => 0
}
if (id.startsWith("GROUP:") && count >= 1) {
val members = mutable.Set[String]()
var i = 0
while (i < count) {
val member = props.getProperty("member" + i, "").trim
if (
member.nonEmpty &&
users.contains(member.toLowerCase)
) {
members += users(member.toLowerCase).username
}
i += 1
}
if (members.nonEmpty) {
chatMembers.put(id, members)
chatNames.put(id, name)
chatGroups.put(id, true)
chatHistory.getOrElseUpdate(
id,
mutable.ListBuffer[ChatMessage]()
)
}
}
} catch {
case _: Throwable =>
}
}
}
} finally {
stream.close()
}
}
// ========================================================
// PERMANENT MEDIA INDEX
// ========================================================
private def loadMediaIndex(): Unit = {
mediaIndex.clear()
if (!Files.exists(mediaIndexFile)) return
try {
val lines = Files.readAllLines(
mediaIndexFile,
StandardCharsets.UTF_8
)
val it = lines.iterator()
while (it.hasNext) {
val line = it.next()
val p = line.split("\\|", -1)
if (p.length >= 7) {
try {
val fileId = Util.decode(p(0))
val chatId = Util.decode(p(1))
val sender = Util.decode(p(2))
val time = Util.decode(p(3))
val fileName = Util.decode(p(4))
val kind = Util.decode(p(5))
val size = p(6).toLong
val stored = mediaFolder.resolve(
fileId + "_" + Util.safeFilename(fileName)
)
if (Files.exists(stored)) {
mediaIndex.put(
fileId,
MediaInfo(
fileId,
chatId,
sender,
time,
fileName,
kind,
size,
stored
)
)
}
} catch {
case _: Throwable =>
}
}
}
} catch {
case _: Throwable =>
}
}
private def saveMediaIndex(
info: MediaInfo
): Unit = {
try {
mediaIndex.put(
info.fileId,
info
)
Files.createDirectories(mediaFolder)
val line =
Util.encode(info.fileId) + "|" +
Util.encode(info.chatId) + "|" +
Util.encode(info.sender) + "|" +
Util.encode(info.time) + "|" +
Util.encode(info.fileName) + "|" +
Util.encode(info.kind) + "|" +
info.size
Files.write(
mediaIndexFile,
(line + "\n").getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
)
} catch {
case _: Throwable =>
}
}
private def sendMediaHistory(
client: ClientConnection,
chatId: String
): Unit = {
val list =
mediaIndex.values
.filter(_.chatId == chatId)
.toSeq
.sortBy(_.time)
.takeRight(100)
val out = new StringBuilder(
"MEDIA_HISTORY|" +
Util.encode(chatId) +
"|" +
list.length
)
list.foreach { info =>
out.append("|")
out.append(Util.encode(info.fileId))
out.append("|")
out.append(Util.encode(info.sender))
out.append("|")
out.append(Util.encode(info.time))
out.append("|")
out.append(Util.encode(info.fileName))
out.append("|")
out.append(Util.encode(info.kind))
out.append("|")
out.append(info.size)
}
client.send(out.toString)
}
private def sendStoredMedia(
client: ClientConnection,
info: MediaInfo
): Unit = {
val thread =
new Thread(
new Runnable {
override def run(): Unit = {
try {
client.send(
"MEDIA_BEGIN|" +
Util.encode(info.chatId) +
"|" +
Util.encode(info.fileId) +
"|" +
Util.encode(info.fileName) +
"|" +
Util.encode(info.kind) +
"|" +
info.size
)
val input =
Files.newInputStream(
info.storedPath
)
try {
val buffer =
new Array[Byte](MEDIA_CHUNK_BYTES)
var count =
input.read(buffer)
while (count >= 0) {
if (count > 0) {
val b64 =
Base64.getUrlEncoder
.withoutPadding()
.encodeToString(
java.util.Arrays.copyOf(
buffer,
count
)
)
client.send(
"MEDIA_CHUNK|" +
Util.encode(info.fileId) +
"|" +
b64
)
}
count =
input.read(buffer)
}
} finally {
input.close()
}
client.send(
"MEDIA_END|" +
Util.encode(info.fileId)
)
} catch {
case _: Throwable =>
}
}
},
"Yadnesh-Media-Sender"
)
thread.setDaemon(true)
thread.start()
}
// ========================================================
// 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
)
sendMediaHistory(
client,
chatId
)
}
// ========================================================
// 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 _ =>
}
}
if (name != null && name.nonEmpty) {
screenSharing -= name
}
loginChallenges.remove(
client.socket
)
registerChallenges.remove(
client.socket
)
recoveryChallenges.remove(
client.socket
)
mediaUploads.remove(client.socket).foreach { upload =>
try upload.out.close() catch { case _: Throwable => }
try Files.deleteIfExists(upload.tempPath) catch { case _: Throwable => }
}
}
client.close()
}
private def groupIdsLike(
chatId: String
): Boolean = {
chatId != null && chatId.startsWith("GROUP:")
}
// ========================================================
// 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
}
// ======================================================
// REELS LIST - ONLY MEDIA THE USER CAN ACCESS
// ======================================================
if (command == "REELS_LIST") {
lock.synchronized {
val videos =
mediaIndex.values
.filter { info =>
info.kind == "video" &&
chatMembers
.get(info.chatId)
.exists(_.contains(currentUser)) &&
Files.exists(info.storedPath)
}
.toSeq
.sortBy(_.time)
.reverse
.take(100)
val out =
new StringBuilder(
"REELS_LIST|" + videos.length
)
videos.foreach { info =>
val chatTitle =
if (chatGroups.getOrElse(info.chatId, false)) {
"#" + chatNames.getOrElse(info.chatId, "Group")
} else {
val members =
chatMembers.getOrElse(
info.chatId,
mutable.Set[String]()
)
members
.find(!_.equalsIgnoreCase(currentUser))
.map("@" + _)
.getOrElse("@Chat")
}
out.append("|")
out.append(Util.encode(info.fileId))
out.append("|")
out.append(Util.encode(info.sender))
out.append("|")
out.append(Util.encode(info.time))
out.append("|")
out.append(Util.encode(info.fileName))
out.append("|")
out.append(Util.encode(chatTitle))
out.append("|")
out.append(info.size)
}
client.send(out.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
}
// ======================================================
// LIVE SCREEN SHARE - FRIEND REQUEST FLOW
// ======================================================
if (command == "SCREEN_SHARE_ON") {
lock.synchronized { screenSharing += currentUser }
client.send("SCREEN_SHARE_STATE|1")
return
}
if (command == "SCREEN_SHARE_OFF") {
lock.synchronized {
screenSharing -= currentUser
pendingScreenRequests.remove(currentUser.toLowerCase)
}
client.send("SCREEN_SHARE_STATE|0")
sessions.values.foreach(_.send("SCREEN_SHARE_STOPPED|" + Util.encode(currentUser)))
return
}
if (command == "SCREEN_REQUEST") {
if (p.length < 3) return
val target = Util.decode(p(1)).trim
val supplied = Util.decode(p(2))
val correctPassword = Util.secureEquals(supplied, SCREEN_SHARE_PASSWORD)
val allowedFriend = isFriend(currentUser, target)
if (correctPassword && allowedFriend) {
lock.synchronized {
pendingScreenRequests.put(target.toLowerCase, currentUser)
}
sendToUser(target, "SCREEN_SHARE_REQUEST|" + Util.encode(currentUser))
client.send("SCREEN_REQUEST_SENT|" + Util.encode(target))
} else {
client.send("ERROR|" + Util.encode("Live screen request denied."))
}
return
}
if (command == "SCREEN_APPROVE") {
if (p.length < 2) return
val viewer = Util.decode(p(1)).trim
val validRequest = lock.synchronized {
pendingScreenRequests.get(currentUser.toLowerCase).exists(_.equalsIgnoreCase(viewer))
}
val allowedFriend = isFriend(currentUser, viewer)
if (validRequest && allowedFriend) {
lock.synchronized {
pendingScreenRequests.remove(currentUser.toLowerCase)
screenSharing += currentUser
}
sendToUser(currentUser, "SCREEN_SHARE_STATE|1")
sendToUser(viewer, "SCREEN_ALLOWED|" + Util.encode(currentUser))
sendToUser(currentUser, "SCREEN_START_CAPTURE|" + Util.encode(viewer))
} else {
client.send("ERROR|" + Util.encode("That live-screen request is no longer valid."))
}
return
}
if (command == "SCREEN_REJECT") {
if (p.length < 2) return
val viewer = Util.decode(p(1)).trim
val validRequest = lock.synchronized {
pendingScreenRequests.get(currentUser.toLowerCase).exists(_.equalsIgnoreCase(viewer))
}
if (validRequest) {
lock.synchronized { pendingScreenRequests.remove(currentUser.toLowerCase) }
sendToUser(viewer, "SCREEN_REQUEST_DENIED|" + Util.encode(currentUser))
}
return
}
if (command == "SCREEN_STOP_VIEW") {
if (p.length < 2) return
val target = Util.decode(p(1)).trim
sendToUser(target, "SCREEN_STOP_CAPTURE|" + Util.encode(currentUser))
client.send("SCREEN_WATCH_STOPPED|" + Util.encode(target))
return
}
if (command == "SCREEN_FRAME") {
if (p.length < 3) return
val viewer = Util.decode(p(1))
val payload = p(2)
val validFriend = isFriend(currentUser, viewer)
val hostSharing = lock.synchronized { screenSharing.contains(currentUser) }
if (validFriend && hostSharing && payload.length <= MAX_SCREEN_FRAME_BYTES * 2) {
sendToUser(viewer, "SCREEN_FRAME|" + Util.encode(currentUser) + "|" + payload)
}
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
) &&
isFriend(
currentUser,
name
) &&
!isMutuallyBlocked(
currentUser,
name
)
) {
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
]()
)
saveGroup(chatId)
members.foreach {
member =>
addChatToUser(
chatId,
member
)
}
client.send(
"GROUP_CREATED|" +
Util.encode(
groupName
)
)
}
}
return
}
// ======================================================
// GROUP INFO
// ======================================================
if (command == "GROUP_INFO") {
if (p.length < 2) return
val chatId = Util.decode(p(1))
lock.synchronized {
val members =
chatMembers.get(chatId)
if (
groupIdsLike(chatId) &&
members.exists(_.contains(currentUser))
) {
val list =
members.get.toSeq.sortBy(_.toLowerCase)
val out = new StringBuilder(
"GROUP_INFO|" +
Util.encode(chatId) +
"|" +
Util.encode(chatNames.getOrElse(chatId, "Group")) +
"|" +
list.length
)
list.foreach { name =>
out.append("|")
out.append(Util.encode(name))
out.append("|")
out.append(
if (sessions.contains(name.toLowerCase)) "1" else "0"
)
}
client.send(out.toString)
} else {
client.send(
"ERROR|" +
Util.encode("Private group not found or access denied.")
)
}
}
return
}
// ======================================================
// ADD FRIEND TO GROUP
// ======================================================
if (command == "GROUP_ADD_FRIEND") {
if (p.length < 4) return
val chatId = Util.decode(p(1))
val count = try p(2).toInt catch { case _: Throwable => 0 }
if (count < 1 || count > 20 || p.length < 3 + count) return
lock.synchronized {
val membersOpt = chatMembers.get(chatId)
if (
!groupIdsLike(chatId) ||
membersOpt.isEmpty ||
!membersOpt.get.contains(currentUser)
) {
client.send(
"ERROR|" +
Util.encode("You are not a member of this private group.")
)
} else {
val members = membersOpt.get
var i = 0
while (i < count) {
val target =
Util.decode(p(3 + i)).trim
if (
users.contains(target.toLowerCase) &&
isFriend(currentUser, target) &&
!isMutuallyBlocked(currentUser, target)
) {
members +=
users(target.toLowerCase).username
}
i += 1
}
saveGroup(chatId)
members.foreach { member =>
addChatToUser(chatId, member)
}
broadcastChat(
chatId,
"GROUP_UPDATED|" +
Util.encode(chatId)
)
}
}
return
}
// ======================================================
// LEAVE GROUP
// ======================================================
if (command == "GROUP_LEAVE") {
if (p.length < 2) return
val chatId = Util.decode(p(1))
lock.synchronized {
chatMembers.get(chatId) match {
case Some(members)
if groupIdsLike(chatId) && members.contains(currentUser) =>
members -= currentUser
if (members.isEmpty) {
chatMembers.remove(chatId)
chatNames.remove(chatId)
chatGroups.remove(chatId)
try {
Files.deleteIfExists(groupFile(chatId))
} catch {
case _: Throwable =>
}
} else {
saveGroup(chatId)
}
client.send(
"GROUP_LEFT|" +
Util.encode(chatId)
)
members.foreach { member =>
member.nonEmpty match {
case true => addChatToUser(chatId, member)
case false => ()
}
}
case _ =>
client.send(
"ERROR|" +
Util.encode("You cannot leave this group.")
)
}
}
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
}
// ======================================================
// MEDIA START
// ======================================================
if (command == "MEDIA_START") {
if (p.length < 6) return
val chatId = Util.decode(p(1))
val fileId = Util.decode(p(2))
val fileName = Util.clean(Util.decode(p(3)), 120)
val kind = Util.decode(p(4))
val size = try p(5).toLong catch { case _: Throwable => -1L }
val allowed =
chatMembers
.get(chatId)
.exists(_.contains(currentUser))
if (
!allowed ||
(kind != "image" && kind != "video") ||
size <= 0L ||
size > MAX_MEDIA_BYTES ||
fileId.isEmpty ||
fileName.isEmpty
) {
client.send(
"ERROR|" +
Util.encode("Media upload denied. Maximum file size is 25 MB.")
)
return
}
lock.synchronized {
mediaUploads.get(client.socket).foreach { old =>
try old.out.close() catch { case _: Throwable => }
try Files.deleteIfExists(old.tempPath) catch { case _: Throwable => }
}
val tempPath =
mediaFolder.resolve(
fileId + ".part"
)
try {
Files.createDirectories(mediaFolder)
val out =
new BufferedOutputStream(
Files.newOutputStream(
tempPath
),
64 * 1024
)
mediaUploads.put(
client.socket,
MediaUpload(
chatId,
fileId,
fileName,
kind,
size,
tempPath,
out,
0L,
currentUser
)
)
client.send(
"MEDIA_READY|" +
Util.encode(fileId)
)
} catch {
case _: Throwable =>
client.send(
"ERROR|" +
Util.encode("Could not start media upload.")
)
}
}
return
}
// ======================================================
// MEDIA CHUNK - fast path.
// No server-wide lock is held while decoding/writing media bytes.
if (command == "MEDIA_CHUNK") {
if (p.length < 3) return
val fileId = Util.decode(p(1))
val chunk = p(2)
val uploadOpt =
mediaUploads.synchronized {
mediaUploads.get(client.socket)
}
uploadOpt match {
case Some(upload) if upload.fileId == fileId =>
upload.synchronized {
try {
val bytes =
Base64.getUrlDecoder.decode(chunk)
if (upload.written + bytes.length <= upload.size) {
upload.out.write(bytes)
upload.written += bytes.length
} else {
try upload.out.close() catch { case _: Throwable => }
try Files.deleteIfExists(upload.tempPath) catch { case _: Throwable => }
mediaUploads.synchronized { mediaUploads.remove(client.socket) }
client.send(
"ERROR|" +
Util.encode("Media upload exceeded its declared size.")
)
}
} catch {
case _: Throwable =>
try upload.out.close() catch { case _: Throwable => }
try Files.deleteIfExists(upload.tempPath) catch { case _: Throwable => }
mediaUploads.synchronized { mediaUploads.remove(client.socket) }
client.send(
"ERROR|" +
Util.encode("Invalid media data.")
)
}
}
case _ =>
client.send(
"ERROR|" +
Util.encode("No matching media upload.")
)
}
return
}
if (command == "MEDIA_END") {
if (p.length < 2) return
val fileId = Util.decode(p(1))
lock.synchronized {
mediaUploads.get(client.socket) match {
case Some(upload)
if upload.fileId == fileId =>
try {
upload.out.close()
if (upload.written != upload.size) {
Files.deleteIfExists(upload.tempPath)
client.send(
"ERROR|" +
Util.encode("Media upload was incomplete.")
)
} else {
val finalPath =
mediaFolder.resolve(
fileId +
"_" +
Util.safeFilename(upload.fileName)
)
Files.move(
upload.tempPath,
finalPath,
StandardCopyOption.REPLACE_EXISTING
)
val info =
MediaInfo(
fileId,
upload.chatId,
upload.sender,
Util.timeNow(),
upload.fileName,
upload.kind,
upload.size,
finalPath
)
saveMediaIndex(info)
val members =
chatMembers.getOrElse(
upload.chatId,
mutable.Set[String]()
)
members.foreach { member =>
sendToUser(
member,
"MEDIA_AVAILABLE|" +
Util.encode(upload.chatId) +
"|" +
Util.encode(upload.fileId) +
"|" +
Util.encode(upload.sender) +
"|" +
Util.encode(info.time) +
"|" +
Util.encode(upload.fileName) +
"|" +
Util.encode(upload.kind) +
"|" +
upload.size
)
}
client.send(
"MEDIA_SAVED|" +
Util.encode(upload.fileId)
)
}
} catch {
case _: Throwable =>
try upload.out.close() catch { case _: Throwable => }
try Files.deleteIfExists(upload.tempPath) catch { case _: Throwable => }
}
mediaUploads.remove(client.socket)
case _ =>
client.send(
"ERROR|" +
Util.encode("No matching media upload.")
)
}
}
return
}
// ======================================================
// REEL OPEN - SERVER CHECKS PRIVATE MEMBERSHIP
// ======================================================
if (command == "REEL_OPEN") {
if (p.length < 2) return
val fileId =
Util.decode(p(1))
lock.synchronized {
mediaIndex.get(fileId) match {
case Some(info)
if info.kind == "video" &&
chatMembers
.get(info.chatId)
.exists(_.contains(currentUser)) &&
Files.exists(info.storedPath) =>
sendStoredMedia(
client,
info
)
case _ =>
client.send(
"ERROR|" +
Util.encode(
"This reel is unavailable or private."
)
)
}
}
return
}
// ======================================================
// MEDIA GET
// ======================================================
if (command == "MEDIA_GET") {
if (p.length < 3) return
val chatId = Util.decode(p(1))
val fileId = Util.decode(p(2))
val allowed =
chatMembers
.get(chatId)
.exists(_.contains(currentUser))
if (!allowed) {
client.send(
"ERROR|" +
Util.encode("You cannot download media from this private chat.")
)
return
}
lock.synchronized {
mediaIndex.get(fileId) match {
case Some(info)
if info.chatId == chatId && Files.exists(info.storedPath) =>
sendStoredMedia(
client,
info
)
case _ =>
client.send(
"ERROR|" +
Util.encode("Media not found.")
)
}
}
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
)
}
}
// ========================================================
// 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
@volatile private var screenShareEnabled =
false
@volatile private var screenCaptureRunning =
false
private val screenCaptureViewers =
mutable.Set[String]()
var username =
""
var password =
""
var registerUsername =
""
var registerPassword =
""
var registerBio =
""
var registerAvatar =
""
var recoveryUsername =
""
var recoverySecret =
""
var recoveryNewPassword =
""
var ownBio =
""
var ownAvatar =
""
private val mediaOutput =
mutable.Map[String, java.io.OutputStream]()
private val mediaPath =
mutable.Map[String, Path]()
private val mediaChat =
mutable.Map[String, String]()
private val mediaName =
mutable.Map[String, String]()
private val mediaKind =
mutable.Map[String, String]()
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
),
1200
)
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(20000L)
} 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 {
this.synchronized {
writer.println(
line
)
writer.flush()
}
} catch {
case _: Throwable =>
}
}
}
// ========================================================
// SEND MEDIA FILE
// ========================================================
def sendFile(
chatId: String,
path: String,
kind: String
): Boolean = {
if (
!connected ||
writer == null
) {
return false
}
try {
val file = Paths.get(path)
if (!Files.exists(file)) return false
val size = Files.size(file)
if (
size <= 0L ||
size > MAX_MEDIA_BYTES
) {
JOptionPane.showMessageDialog(
null,
"Media file must be smaller than 25 MB.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return false
}
if (
kind != "image" &&
kind != "video"
) {
return false
}
val fileId =
UUID.randomUUID().toString
val fileName =
Util.clean(
file.getFileName.toString,
120
)
val thread =
new Thread(
new Runnable {
override def run(): Unit = {
try {
send(
"MEDIA_START|" +
Util.encode(chatId) +
"|" +
Util.encode(fileId) +
"|" +
Util.encode(fileName) +
"|" +
Util.encode(kind) +
"|" +
size
)
val input =
Files.newInputStream(file)
try {
val buffer =
new Array[Byte](MEDIA_CHUNK_BYTES)
var count =
input.read(buffer)
while (count >= 0) {
if (count > 0) {
val b64 =
Base64.getUrlEncoder
.withoutPadding()
.encodeToString(
java.util.Arrays.copyOf(
buffer,
count
)
)
send(
"MEDIA_CHUNK|" +
Util.encode(fileId) +
"|" +
b64
)
}
count =
input.read(buffer)
}
} finally {
input.close()
}
send(
"MEDIA_END|" +
Util.encode(fileId)
)
} catch {
case _: Throwable =>
}
}
},
"Yadnesh-Media-Uploader"
)
thread.setDaemon(true)
thread.start()
true
} catch {
case _: Throwable =>
false
}
}
def sendImageBytes(
chatId: String,
bytes: Array[Byte],
fileName: String
): Boolean = {
if (
bytes == null ||
bytes.length == 0 ||
bytes.length > 500 * 1024
) {
return false
}
try {
val temp =
Files.createTempFile(
"yadnesh_paste_",
".png"
)
Files.write(
temp,
bytes
)
val ok =
sendFile(
chatId,
temp.toString,
"image"
)
val cleaner =
new Thread(
new Runnable {
override def run(): Unit = {
try {
Thread.sleep(15000L)
} catch {
case _: Throwable =>
}
try {
Files.deleteIfExists(temp)
} catch {
case _: Throwable =>
}
}
}
)
cleaner.setDaemon(true)
cleaner.start()
ok
} catch {
case _: Throwable =>
false
}
}
// ========================================================
// LIVE SCREEN SHARE
// ========================================================
def startMyScreenShare(): Unit = {
if (!screenShareEnabled) {
screenShareEnabled = true
send("SCREEN_SHARE_ON")
}
}
def stopMyScreenShare(): Unit = {
screenShareEnabled = false
screenCaptureRunning = false
screenCaptureViewers.synchronized { screenCaptureViewers.clear() }
send("SCREEN_SHARE_OFF")
}
def requestFriendScreen(target: String): Unit = {
requestFriendScreenWithPassword(target, SCREEN_SHARE_PASSWORD)
}
def requestFriendScreenWithPassword(target: String, suppliedPassword: String): Unit = {
if (target != null && target.trim.nonEmpty) {
send(
"SCREEN_REQUEST|" +
Util.encode(target.trim) +
"|" +
Util.encode(if (suppliedPassword == null) "" else suppliedPassword)
)
}
}
private def captureAllDesktop(): BufferedImage = {
val devices = GraphicsEnvironment
.getLocalGraphicsEnvironment
.getScreenDevices
if (devices == null || devices.isEmpty) return null
var union: Rectangle = null
var i = 0
while (i < devices.length) {
val r = devices(i).getDefaultConfiguration.getBounds
union = if (union == null) new Rectangle(r) else union.union(r)
i += 1
}
if (union == null || union.width <= 0 || union.height <= 0) return null
val combined = new BufferedImage(
union.width,
union.height,
BufferedImage.TYPE_INT_RGB
)
val g = combined.createGraphics()
try {
i = 0
while (i < devices.length) {
val device = devices(i)
val r = device.getDefaultConfiguration.getBounds
try {
val robot = new Robot(device)
val part = robot.createScreenCapture(r)
g.drawImage(part, r.x - union.x, r.y - union.y, null)
} catch { case _: Throwable => }
i += 1
}
} finally {
g.dispose()
}
combined
}
private def startCaptureForViewer(viewer: String): Unit = {
if (viewer == null || viewer.isEmpty) return
if (!screenShareEnabled) {
screenShareEnabled = true
send("SCREEN_SHARE_ON")
}
screenCaptureViewers.synchronized { screenCaptureViewers += viewer }
if (screenCaptureRunning) return
screenCaptureRunning = true
val thread = new Thread(new Runnable {
override def run(): Unit = {
try {
while (screenShareEnabled && screenCaptureViewers.synchronized(screenCaptureViewers.nonEmpty)) {
try {
val image = captureAllDesktop()
if (image != null) {
val out = new ByteArrayOutputStream()
ImageIO.write(image, "jpg", out)
out.close()
val bytes = out.toByteArray
if (bytes.length <= MAX_SCREEN_FRAME_BYTES) {
val payload = Base64.getUrlEncoder.withoutPadding().encodeToString(bytes)
val viewers = screenCaptureViewers.synchronized(screenCaptureViewers.toSeq)
viewers.foreach { viewerName =>
send(
"SCREEN_FRAME|" +
Util.encode(viewerName) +
"|" +
payload
)
}
}
}
} catch { case _: Throwable => }
try Thread.sleep(SCREEN_CAPTURE_DELAY_MS) catch { case _: Throwable => }
}
} finally {
screenCaptureRunning = false
}
}
}, "Yadnesh-Screen-Share")
thread.setDaemon(true)
thread.start()
}
// ========================================================
// CLOSE
// ========================================================
def close(): Unit = {
screenShareEnabled = false
screenCaptureRunning = false
screenCaptureViewers.synchronized { screenCaptureViewers.clear() }
try send("SCREEN_SHARE_OFF") catch { case _: Throwable => }
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
if (
line.startsWith("MEDIA_BEGIN|") ||
line.startsWith("MEDIA_CHUNK|")
) {
processEvent(message)
} else {
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 "MEDIA_BEGIN" =>
if (p.length >= 6) {
try {
val chatId = Util.decode(p(1))
val fileId = Util.decode(p(2))
val fileName = Util.decode(p(3))
val kind = Util.decode(p(4))
val size = p(5).toLong
Files.createDirectories(
Paths.get("YadneshLiveChatDownloads")
)
val safe =
Util.clean(
fileName,
120
).replaceAll(
"[^A-Za-z0-9._-]",
"_"
)
val path =
Paths.get("YadneshLiveChatDownloads")
.resolve(
fileId +
"_" +
safe
)
val out =
new BufferedOutputStream(
Files.newOutputStream(
path
),
64 * 1024
)
mediaOutput.put(
fileId,
out
)
mediaPath.put(
fileId,
path
)
mediaChat.put(
fileId,
chatId
)
mediaName.put(
fileId,
fileName
)
mediaKind.put(
fileId,
kind
)
} catch {
case _: Throwable =>
}
}
case "MEDIA_CHUNK" =>
if (p.length >= 3) {
mediaOutput.get(
Util.decode(p(1))
).foreach { out =>
try {
out.write(
Base64.getUrlDecoder.decode(p(2))
)
} catch {
case _: Throwable =>
}
}
}
case "MEDIA_END" =>
if (p.length >= 2) {
val fileId =
Util.decode(p(1))
mediaOutput.remove(fileId).foreach { out =>
try {
out.close()
} catch {
case _: Throwable =>
}
}
val path =
mediaPath.getOrElse(
fileId,
Paths.get("")
)
callback(
"MEDIA_RECEIVED|" +
Util.encode(
mediaChat.getOrElse(fileId, "")
) +
"|" +
Util.encode(
path.toString
) +
"|" +
Util.encode(
mediaName.getOrElse(fileId, "file")
) +
"|" +
Util.encode(
mediaKind.getOrElse(fileId, "file")
) +
"|" +
Util.encode(fileId)
)
}
case "SCREEN_SHARE_REQUEST" =>
callback(line)
case "SCREEN_REQUEST_SENT" =>
callback(line)
case "SCREEN_REQUEST_DENIED" =>
callback(line)
case "SCREEN_WATCH_STOPPED" =>
callback(line)
case "SCREEN_SHARE_STATE" =>
callback(line)
case "SCREEN_START_CAPTURE" =>
if (p.length >= 2) startCaptureForViewer(Util.decode(p(1)))
case "SCREEN_STOP_CAPTURE" =>
if (p.length >= 2) screenCaptureViewers.synchronized { screenCaptureViewers -= Util.decode(p(1)) }
case "SCREEN_ALLOWED" =>
callback(line)
case "SCREEN_SHARE_STOPPED" =>
callback(line)
case "SCREEN_FRAME" =>
callback(line)
case "GROUP_CREATED" =>
callback(line)
case "GROUP_INFO" =>
callback(line)
case "GROUP_UPDATED" =>
callback(line)
case "GROUP_LEFT" =>
callback(line)
case "MEDIA_READY" =>
callback(line)
case "MEDIA_SAVED" =>
callback(line)
case "REELS_LIST" =>
callback(line)
case "MEDIA_AVAILABLE" =>
callback(line)
case "MEDIA_HISTORY" =>
callback(line)
case "MEDIA_RECEIVED" =>
callback(line)
case "FILE_SENT" =>
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,
520
)
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'>FULL AUTOMATIC WIFI / INTERNET 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
)
)
networkStatus.setText(
"NETWORK: AUTOMATIC DETECTION ENABLED"
)
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()
}
// ========================================================
// FULL AUTOMATIC NETWORK ENGINE
// ========================================================
private def networkCachePath(): Path = {
Paths.get("YadneshLiveChatData").resolve("network.properties")
}
private def saveNetworkEndpoint(host: String, port: Int): Unit = {
try {
Files.createDirectories(Paths.get("YadneshLiveChatData"))
val props = new Properties()
props.setProperty("host", host)
props.setProperty("port", port.toString)
val out = Files.newOutputStream(
networkCachePath(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
try {
props.store(out, "YADNESH LIVE CHAT automatic network cache")
} finally {
out.close()
}
} catch {
case _: Throwable =>
}
}
private def loadNetworkEndpoint(): Option[(String, Int)] = {
try {
val path = networkCachePath()
if (!Files.exists(path)) return None
val props = new Properties()
val in = Files.newInputStream(path)
try { props.load(in) } finally { in.close() }
val host = props.getProperty("host", "").trim
val port = try props.getProperty("port", DEFAULT_PORT.toString).trim.toInt
catch { case _: Throwable => -1 }
if (host.nonEmpty && port >= 1 && port <= 65535) Some((host, port)) else None
} catch {
case _: Throwable => None
}
}
private def tryEndpoint(
host: String,
port: Int,
statusText: String
): Boolean = {
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = networkStatus.setText(statusText)
})
val ok = client.connect(host, port)
if (ok) {
discoveredHost = host
discoveredPort = port
networkReady = true
saveNetworkEndpoint(host, port)
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
networkStatus.setText("NETWORK: AUTO CONNECTED ? " + host + ":" + port)
}
})
}
ok
}
private def findServerByLocalSubnet(): Option[(String, Int)] = {
val candidates = mutable.ListBuffer[String]()
val seenCandidates = mutable.HashSet[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 parts = address.getHostAddress().split("\\.")
if (parts.length == 4) {
val a = parts(0).toInt
val b = parts(1).toInt
val c = parts(2).toInt
var host = 1
while (host <= 254) {
val candidate = a + "." + b + "." + c + "." + host
if (candidate != address.getHostAddress() && seenCandidates.add(candidate)) {
candidates += candidate
}
host += 1
}
}
}
}
}
}
} catch {
case _: Throwable =>
}
if (candidates.isEmpty) return None
val queue = mutable.Queue[String]()
queue ++= candidates
val found = new java.util.concurrent.atomic.AtomicReference[Option[(String, Int)]](None)
val stop = new java.util.concurrent.atomic.AtomicBoolean(false)
val workerCount = math.min(8, math.max(1, candidates.size))
val workers = mutable.ListBuffer[Thread]()
var i = 0
while (i < workerCount) {
val worker = new Thread(new Runnable {
override def run(): Unit = {
var running = true
while (running && !stop.get()) {
val next = queue.synchronized {
if (queue.nonEmpty) Some(queue.dequeue()) else None
}
next match {
case None => running = false
case Some(host) =>
if (!stop.get()) {
var socket: Socket = null
try {
socket = new Socket()
socket.setSoTimeout(160)
socket.connect(new InetSocketAddress(host, DEFAULT_PORT), 160)
val out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream, StandardCharsets.UTF_8),
true
)
val in = new BufferedReader(
new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8)
)
out.println("PING")
out.flush()
if (in.readLine() == "PONG") {
found.compareAndSet(None, Some((host, DEFAULT_PORT)))
stop.set(true)
}
} catch {
case _: Throwable =>
} finally {
if (socket != null) { try socket.close() catch { case _: Throwable => } }
}
}
}
}
}
}, "Yadnesh-LAN-Scan-" + i)
worker.setDaemon(true)
workers += worker
worker.start()
i += 1
}
val deadline = System.currentTimeMillis() + 950L
while (System.currentTimeMillis() < deadline && found.get().isEmpty) {
try Thread.sleep(60L) catch { case _: Throwable => }
}
stop.set(true)
found.get()
}
private def startAutomaticNetwork(): Unit = {
networkStatus.setText("NETWORK: AUTO-DETECTING Wi-Fi / LAN / INTERNET...")
val thread = new Thread(new Runnable {
override def run(): Unit = {
// A. Previously successful endpoint, without asking the user.
loadNetworkEndpoint() match {
case Some((host, port)) =>
if (tryEndpoint(host, port, "NETWORK: TRYING SAVED CONNECTION...")) return
case None =>
}
// B. UDP LAN discovery.
var found = LanDiscovery.findServer(300)
if (found.isEmpty) {
// C. LAN subnet scan fallback if broadcast is blocked.
found = findServerByLocalSubnet()
}
found match {
case Some((host, port)) =>
if (tryEndpoint(host, port, "NETWORK: LAN SERVER FOUND AUTOMATICALLY...")) return
case None =>
}
// D. Optional built-in public server for Internet deployments.
val publicHost = AUTO_PUBLIC_SERVER_HOST.trim
if (publicHost.nonEmpty) {
if (tryEndpoint(publicHost, AUTO_PUBLIC_SERVER_PORT,
"NETWORK: CONNECTING TO INTERNET SERVER AUTOMATICALLY...")) return
}
// E. Nothing reachable: start this PC as the host automatically.
val started = Server.start(DEFAULT_PORT)
if (started && tryEndpoint("127.0.0.1", DEFAULT_PORT,
"NETWORK: STARTING THIS PC AS HOST AUTOMATICALLY...")) {
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
networkStatus.setText(
"NETWORK: AUTO HOST READY ? " + Util.localIPs().mkString(", ") + ":" + DEFAULT_PORT
)
}
})
return
}
networkReady = false
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = networkStatus.setText("NETWORK: AUTOMATIC DISCOVERY FAILED")
})
}
}, "Yadnesh-Full-Auto-Network")
thread.setDaemon(true)
thread.start()
}
// ========================================================
// CONNECT
// ========================================================
private def connect(): Boolean = {
if (
client.connected
) {
return true
}
if (
!networkReady
) {
JOptionPane.showMessageDialog(
frame,
"Automatic network detection is still running. Try LOGIN again in 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 imageButton =
new JButton(
"ADD PHOTO"
)
private val pasteImageButton =
new JButton(
"PASTE PHOTO"
)
private val videoButton =
new JButton(
"SELECT VIDEO"
)
private val deselectVideoButton =
new JButton(
"DESELECT VIDEO"
)
private val sendSelectedVideoButton =
new JButton(
"SEND VIDEO"
)
private val startRecordButton =
new JButton(
"START SCREEN REC"
)
private val stopRecordButton =
new JButton(
"STOP & SEND REC"
)
private val reelsButton =
new JButton(
"REELS"
)
private val sendReelsButton =
new JButton(
"REELS"
)
private val allUsersButton =
new JButton(
"ALL USERS A-Z"
)
private val myScreenButton =
new JButton(
"START MY LIVE SCREEN"
)
private val friendScreenButton =
new JButton(
"SEE FRIEND LIVE SCREEN"
)
private val stopScreenButton =
new JButton(
"STOP WATCHING"
)
private var screenViewerFrame: JFrame = null
private val screenViewerLabel =
new JLabel(
"Waiting for live screen...",
SwingConstants.CENTER
)
private val selectedMediaLabel =
new JLabel(
"VIDEO: nothing selected"
)
@volatile private var screenRecording =
false
private var screenRecordThread:
Thread = null
private var selectedVideoPath =
""
private val pendingReelFileIds =
mutable.Set[String]()
private val reelItems =
mutable.Map[String, ReelItem]()
private var reelsModelRef:
DefaultListModel[ReelItem] = null
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 groupInfoButton =
new JButton(
"GROUP INFO"
)
private val groupAddButton =
new JButton(
"ADD FRIEND TO GROUP"
)
private val groupLeaveButton =
new JButton(
"LEAVE 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]()
@volatile private var typingSent =
false
private val typingTimer =
new javax.swing.Timer(900, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (currentChat != null && currentChat.nonEmpty && typingSent) {
client.send("TYPING|" + Util.encode(currentChat) + "|0")
typingSent = false
}
}
})
// ========================================================
// 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
)
friendsList.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(
2,
4,
4,
4
)
)
leftActions.add(
groupButton
)
leftActions.add(
groupInfoButton
)
leftActions.add(
groupAddButton
)
leftActions.add(
groupLeaveButton
)
leftActions.add(
friendsButton
)
leftActions.add(
requestsButton
)
leftActions.add(
allUsersButton
)
leftActions.add(
reelsButton
)
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
)
selectedMediaLabel.setBorder(
BorderFactory.createEmptyBorder(2, 5, 2, 5)
)
sendPanel.add(
selectedMediaLabel,
BorderLayout.NORTH
)
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(
8,
1,
5,
5
)
)
profileButtons.add(openChatButton)
profileButtons.add(friendScreenButton)
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()
if (typingSent) {
client.send("TYPING|" + Util.encode(chat.id) + "|0")
typingSent = false
}
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) {
screenRecording = false
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")
}
}
}
}
)
imageButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
sendImageFromFile()
}
}
)
pasteImageButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
sendPastedImage()
}
}
)
videoButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
selectVideoFile()
}
}
)
deselectVideoButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
deselectVideo()
}
}
)
sendSelectedVideoButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
sendSelectedVideo()
}
}
)
startRecordButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startScreenRecording()
}
}
)
stopRecordButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
stopScreenRecordingAndSend()
}
}
)
reelsButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
openReelsWindow()
}
}
)
sendReelsButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
openReelsWindow()
}
}
)
allUsersButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
showAllUsersAZ()
}
}
)
groupInfoButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
groupInfo()
}
}
)
groupAddButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
addFriendsToCurrentGroup()
}
}
)
groupLeaveButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
leaveCurrentGroup()
}
}
)
friendScreenButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
requestFriendScreen()
}
}
)
messageField.getDocument.addDocumentListener(
new DocumentListener {
private def changed(): Unit = {
if (currentChat != null && currentChat.nonEmpty && messageField.getText.nonEmpty) {
if (!typingSent) {
client.send("TYPING|" + Util.encode(currentChat) + "|1")
typingSent = true
}
typingTimer.restart()
} else {
typingTimer.stop()
if (typingSent && currentChat != null && currentChat.nonEmpty) {
client.send("TYPING|" + Util.encode(currentChat) + "|0")
typingSent = false
}
}
}
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()
if (typingSent) {
client.send("TYPING|" + Util.encode(currentChat) + "|0")
typingSent = false
}
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 =
friendsList
.getSelectedValuesList()
if (
selected == null ||
selected.size() < 1
) {
JOptionPane.showMessageDialog(
frame,
"Select one or more FRIENDS.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val name =
JOptionPane.showInputDialog(
frame,
"Permanent private 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
)
}
// ========================================================
// SEND IMAGE FROM FILE
// ========================================================
private def sendImageFromFile(): Unit = {
if (currentChat == null || currentChat.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"First open a private chat or group.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val chooser = new JFileChooser()
if (
chooser.showOpenDialog(frame) ==
JFileChooser.APPROVE_OPTION
) {
val name =
chooser.getSelectedFile.getName.toLowerCase
if (
!name.endsWith(".png") &&
!name.endsWith(".jpg") &&
!name.endsWith(".jpeg") &&
!name.endsWith(".gif") &&
!name.endsWith(".bmp")
) {
JOptionPane.showMessageDialog(
frame,
"Choose an image file.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
client.sendFile(
currentChat,
chooser.getSelectedFile.getAbsolutePath,
"image"
)
}
}
// ========================================================
// PASTE IMAGE
// ========================================================
private def sendPastedImage(): Unit = {
if (currentChat == null || currentChat.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"First open a chat.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val clipboardData =
Util.clipboardImageBase64()
if (clipboardData.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"Copy an image first, then press PASTE PHOTO.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
try {
val bytes =
Base64.getUrlDecoder.decode(
clipboardData
)
client.sendImageBytes(
currentChat,
bytes,
"pasted_image.png"
)
} catch {
case _: Throwable =>
JOptionPane.showMessageDialog(
frame,
"Could not paste image.",
APP_NAME,
JOptionPane.ERROR_MESSAGE
)
}
}
// ========================================================
// SELECT VIDEO / DESELECT / SEND
// ========================================================
private def selectVideoFile(): Unit = {
if (currentChat == null || currentChat.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"First open a private chat or group.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val chooser = new JFileChooser()
chooser.setDialogTitle("SELECT VIDEO")
if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
val file = chooser.getSelectedFile
val name = file.getName.toLowerCase
if (
!name.endsWith(".mp4") &&
!name.endsWith(".webm") &&
!name.endsWith(".mkv") &&
!name.endsWith(".avi") &&
!name.endsWith(".mov") &&
!name.endsWith(".m4v") &&
!name.endsWith(".gif") &&
!name.endsWith(".mjpeg")
) {
JOptionPane.showMessageDialog(
frame,
"Choose MP4, WEBM, MKV, AVI, MOV, M4V, GIF or MJPEG.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
try {
val size = Files.size(file.toPath)
if (size <= 0L || size > MAX_MEDIA_BYTES) {
JOptionPane.showMessageDialog(
frame,
"Video must be between 1 byte and 25 MB.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
} catch {
case _: Throwable => return
}
selectedVideoPath =
file.getAbsolutePath
selectedMediaLabel.setText(
"VIDEO SELECTED: " + Util.safeFilename(file.getName)
)
deselectVideoButton.setEnabled(true)
sendSelectedVideoButton.setEnabled(true)
}
}
private def deselectVideo(): Unit = {
selectedVideoPath = ""
selectedMediaLabel.setText("VIDEO: nothing selected")
deselectVideoButton.setEnabled(false)
sendSelectedVideoButton.setEnabled(false)
}
private def sendSelectedVideo(): Unit = {
if (currentChat == null || currentChat.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"First open a private chat or group.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (selectedVideoPath == null || selectedVideoPath.isEmpty) {
selectVideoFile()
return
}
if (client.sendFile(currentChat, selectedVideoPath, "video")) {
deselectVideo()
}
}
// ========================================================
// SCREEN RECORDING -> ANIMATED GIF -> LIVE CHAT MEDIA
// ========================================================
private def startScreenRecording(): Unit = {
if (currentChat == null || currentChat.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"First open a private chat or group.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (screenRecording) return
val targetChat = currentChat
try {
val configuration =
GraphicsEnvironment
.getLocalGraphicsEnvironment
.getDefaultScreenDevice
.getDefaultConfiguration
val bounds = configuration.getBounds
val robot = new Robot()
val outputPath =
Files.createTempFile(
"yadnesh_screen_record_",
".gif"
)
screenRecording = true
startRecordButton.setEnabled(false)
stopRecordButton.setEnabled(true)
selectedMediaLabel.setText(
"SCREEN REC: recording... (maximum about 9 seconds)"
)
screenRecordThread =
new Thread(
new Runnable {
override def run(): Unit = {
var output: ImageOutputStream = null
var gif: Util.AnimatedGifWriter = null
var failedMessage = ""
try {
output =
ImageIO.createImageOutputStream(
outputPath.toFile
)
gif =
new Util.AnimatedGifWriter(
output,
200
)
val startedAt =
System.currentTimeMillis()
while (
screenRecording &&
System.currentTimeMillis() - startedAt < 9000L
) {
val raw =
robot.createScreenCapture(
new Rectangle(
bounds
)
)
val maxW = 720
val maxH = 405
val scale =
math.min(
1.0,
math.min(
maxW.toDouble / math.max(1, raw.getWidth),
maxH.toDouble / math.max(1, raw.getHeight)
)
)
val width =
math.max(
1,
(raw.getWidth * scale).toInt
)
val height =
math.max(
1,
(raw.getHeight * scale).toInt
)
val frameImage =
new BufferedImage(
width,
height,
BufferedImage.TYPE_INT_RGB
)
val graphics =
frameImage.createGraphics()
try {
graphics.drawImage(
raw,
0,
0,
width,
height,
null
)
} finally {
graphics.dispose()
}
gif.writeFrame(frameImage)
try {
Thread.sleep(280L)
} catch {
case _: Throwable =>
}
}
} catch {
case ex: Throwable =>
failedMessage =
if (ex.getMessage == null)
"Unknown screen-recording error."
else
ex.getMessage
} finally {
if (gif != null) {
try gif.close() catch { case _: Throwable => }
} else if (output != null) {
try output.close() catch { case _: Throwable => }
}
screenRecording = false
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
startRecordButton.setEnabled(true)
stopRecordButton.setEnabled(false)
if (failedMessage.nonEmpty) {
selectedMediaLabel.setText(
"SCREEN REC ERROR"
)
JOptionPane.showMessageDialog(
frame,
"Screen recording failed: " + failedMessage,
APP_NAME,
JOptionPane.ERROR_MESSAGE
)
} else if (Files.exists(outputPath)) {
try {
val size = Files.size(outputPath)
if (size > 0L && size <= MAX_MEDIA_BYTES) {
selectedMediaLabel.setText(
"SCREEN REC: sending..."
)
client.sendFile(
targetChat,
outputPath.toString,
"video"
)
} else {
selectedMediaLabel.setText(
"SCREEN REC: recording too large or empty"
)
}
} catch {
case _: Throwable =>
}
}
val cleaner =
new Thread(
new Runnable {
override def run(): Unit = {
try Thread.sleep(30000L) catch { case _: Throwable => }
try Files.deleteIfExists(outputPath) catch { case _: Throwable => }
}
},
"Yadnesh-Screen-Recording-Cleaner"
)
cleaner.setDaemon(true)
cleaner.start()
}
}
)
}
}
},
"Yadnesh-Screen-Recorder"
)
screenRecordThread.setDaemon(true)
screenRecordThread.start()
} catch {
case ex: Throwable =>
screenRecording = false
startRecordButton.setEnabled(true)
stopRecordButton.setEnabled(false)
JOptionPane.showMessageDialog(
frame,
"Screen recording is not available: " + ex.getMessage,
APP_NAME,
JOptionPane.ERROR_MESSAGE
)
}
}
private def stopScreenRecordingAndSend(): Unit = {
if (screenRecording) {
screenRecording = false
selectedMediaLabel.setText(
"SCREEN REC: stopping and preparing media..."
)
}
}
// ========================================================
// ALL USERS A-Z
// ========================================================
private def showAllUsersAZ(): Unit = {
searchField.setText("")
searchPeople()
selectedMediaLabel.setText(
"ALL USERS A-Z: every registered account + LIVE status"
)
}
// ========================================================
// REELS VIEWER
// ========================================================
private def openReelsWindow(): Unit = {
val dialog =
new JDialog(
frame,
"YADNESH REELS",
false
)
dialog.setSize(
900,
620
)
dialog.setLocationRelativeTo(frame)
val model =
new DefaultListModel[ReelItem]()
reelsModelRef =
model
val list =
new JList[ReelItem](model)
list.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION
)
val info =
new JLabel(
"REELS: videos from chats/groups you are allowed to access. Double-click a reel to play."
)
val play =
new JButton(
"PLAY SELECTED"
)
val refresh =
new JButton(
"REFRESH REELS"
)
val close =
new JButton(
"CLOSE"
)
val buttons =
new JPanel(
new GridLayout(
1,
3,
6,
6
)
)
buttons.add(play)
buttons.add(refresh)
buttons.add(close)
val root =
new JPanel(
new BorderLayout(
8,
8
)
)
root.setBorder(
BorderFactory.createEmptyBorder(
8,
8,
8,
8
)
)
root.add(
info,
BorderLayout.NORTH
)
root.add(
new JScrollPane(list),
BorderLayout.CENTER
)
root.add(
buttons,
BorderLayout.SOUTH
)
def load(): Unit = {
client.send("REELS_LIST")
}
def playSelected(): Unit = {
val item =
list.getSelectedValue
if (item == null) {
JOptionPane.showMessageDialog(
dialog,
"Select a reel first.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
} else {
pendingReelFileIds += item.fileId
client.send(
"REEL_OPEN|" +
Util.encode(item.fileId)
)
}
}
play.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
playSelected()
}
}
)
refresh.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
load()
}
}
)
close.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
dialog.dispose()
reelsModelRef = null
}
}
)
list.addMouseListener(
new MouseAdapter {
override def mouseClicked(e: MouseEvent): Unit = {
if (e.getClickCount >= 2) playSelected()
}
}
)
dialog.setContentPane(root)
dialog.setVisible(true)
load()
}
// ========================================================
// SEND VIDEO
// ========================================================
private def sendVideoFile(): Unit = {
if (currentChat == null || currentChat.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"First open a chat.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val chooser = new JFileChooser()
if (
chooser.showOpenDialog(frame) ==
JFileChooser.APPROVE_OPTION
) {
val name =
chooser.getSelectedFile.getName.toLowerCase
if (
!name.endsWith(".mp4") &&
!name.endsWith(".webm") &&
!name.endsWith(".mkv") &&
!name.endsWith(".avi") &&
!name.endsWith(".mov") &&
!name.endsWith(".m4v")
) {
JOptionPane.showMessageDialog(
frame,
"Choose a video file such as MP4/MKV/WEBM/AVI/MOV.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
client.sendFile(
currentChat,
chooser.getSelectedFile.getAbsolutePath,
"video"
)
}
}
// ========================================================
// GROUP INFO
// ========================================================
private def groupInfo(): Unit = {
if (
currentChat == null ||
!currentChat.startsWith("GROUP:")
) {
JOptionPane.showMessageDialog(
frame,
"Open a group chat first.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
client.send(
"GROUP_INFO|" +
Util.encode(currentChat)
)
}
// ========================================================
// ADD FRIENDS TO GROUP
// ========================================================
private def addFriendsToCurrentGroup(): Unit = {
if (
currentChat == null ||
!currentChat.startsWith("GROUP:")
) {
JOptionPane.showMessageDialog(
frame,
"Open a group chat first.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val selected =
friendsList.getSelectedValuesList()
if (
selected == null ||
selected.size() < 1
) {
JOptionPane.showMessageDialog(
frame,
"Select one or more friends first.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val out =
new StringBuilder(
"GROUP_ADD_FRIEND|" +
Util.encode(currentChat) +
"|" +
selected.size()
)
var i = 0
while (i < selected.size()) {
out.append("|")
out.append(
Util.encode(
selected.get(i).username
)
)
i += 1
}
client.send(
out.toString
)
}
// ========================================================
// LEAVE GROUP
// ========================================================
private def leaveCurrentGroup(): Unit = {
if (
currentChat == null ||
!currentChat.startsWith("GROUP:")
) {
JOptionPane.showMessageDialog(
frame,
"Select a group first.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val result =
JOptionPane.showConfirmDialog(
frame,
"Leave this private group?",
APP_NAME,
JOptionPane.YES_NO_OPTION
)
if (
result == JOptionPane.YES_OPTION
) {
client.send(
"GROUP_LEAVE|" +
Util.encode(currentChat)
)
}
}
// ========================================================
// 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
)
}
// ========================================================
// SMOOTH CHAT UI BUFFER
// ========================================================
private val MAX_CHAT_UI_CHARS =
140000
private def appendChatLine(
value: String
): Unit = {
chatArea.append(
if (value == null) "" else value
)
val length =
chatArea.getDocument.getLength
if (length > MAX_CHAT_UI_CHARS) {
try {
chatArea.getDocument.remove(
0,
length - MAX_CHAT_UI_CHARS
)
} catch {
case _: Throwable =>
}
}
chatArea.setCaretPosition(
chatArea.getDocument.getLength
)
}
private def requestFriendScreen(): Unit = {
if (selectedPerson == null || selectedPerson.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"Select a FRIEND first.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val password = new JPasswordField()
password.setText(SCREEN_SHARE_PASSWORD)
password.setEchoChar('?')
val panel = new JPanel(new BorderLayout(8, 8))
panel.add(new JLabel("LIVE SCREEN PASSWORD:"), BorderLayout.NORTH)
panel.add(password, BorderLayout.CENTER)
panel.add(
new JLabel("Friend will receive: START YOUR LIVE SCREEN"),
BorderLayout.SOUTH
)
val result = JOptionPane.showConfirmDialog(
frame,
panel,
"SEE FRIEND LIVE SCREEN",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE
)
if (result == JOptionPane.OK_OPTION) {
client.requestFriendScreenWithPassword(
selectedPerson,
new String(password.getPassword)
)
}
}
private def showScreenViewer(host: String): Unit = {
if (screenViewerFrame == null || !screenViewerFrame.isDisplayable) {
screenViewerFrame = new JFrame("LIVE SCREEN - " + host)
screenViewerFrame.setSize(1100, 750)
screenViewerFrame.setLocationRelativeTo(frame)
screenViewerFrame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
screenViewerFrame.add(new JScrollPane(screenViewerLabel), BorderLayout.CENTER)
}
screenViewerFrame.setVisible(true)
}
private def renderScreenFrame(host: String, payload: String): Unit = {
showScreenViewer(host)
val bytesPayload = payload
val worker = new Thread(new Runnable {
override def run(): Unit = {
try {
val bytes = Base64.getUrlDecoder.decode(bytesPayload)
val image = ImageIO.read(new ByteArrayInputStream(bytes))
if (image != null) {
val icon = new javax.swing.ImageIcon(image)
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
if (screenViewerFrame != null && screenViewerFrame.isDisplayable) {
screenViewerLabel.setText("")
screenViewerLabel.setIcon(icon)
}
}
})
}
} catch { case _: Throwable => }
}
}, "Yadnesh-Screen-Viewer")
worker.setDaemon(true)
worker.start()
}
// ========================================================
// 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
}
}
var onlinePeople = 0
var peopleIndex = 0
while (peopleIndex < peopleModel.size()) {
if (peopleModel.getElementAt(peopleIndex).online) {
onlinePeople += 1
}
peopleIndex += 1
}
onlineSummary.setText(
" USERS: " + peopleModel.size() +
" ONLINE: " + onlinePeople + " "
)
// ----------------------------------------------------
// 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))
}
}
// ----------------------------------------------------
// 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("")
appendChatLine(
"[" +
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 "SCREEN_SHARE_REQUEST" =>
if (p.length >= 2) {
val viewer = Util.decode(p(1))
val start = JOptionPane.showConfirmDialog(
frame,
viewer + " wants to see your full desktop live.\n\n" +
"Press START YOUR LIVE SCREEN to begin sharing.",
"START YOUR LIVE SCREEN",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE
)
if (start == JOptionPane.YES_OPTION) {
client.send("SCREEN_APPROVE|" + Util.encode(viewer))
} else {
client.send("SCREEN_REJECT|" + Util.encode(viewer))
}
}
case "SCREEN_REQUEST_SENT" =>
if (p.length >= 2) {
appendChatLine(
"LIVE SCREEN: request sent to " + Util.decode(p(1)) + ". Waiting for START YOUR LIVE SCREEN.\n"
)
}
case "SCREEN_REQUEST_DENIED" =>
val host = if (p.length >= 2) Util.decode(p(1)) else selectedPerson
JOptionPane.showMessageDialog(
frame,
host + " did not start live screen sharing.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
case "SCREEN_WATCH_STOPPED" =>
if (screenViewerFrame != null) {
try screenViewerFrame.dispose() catch { case _: Throwable => }
screenViewerFrame = null
}
case "SCREEN_SHARE_STATE" =>
()
case "SCREEN_ALLOWED" =>
val host = if (p.length >= 2) Util.decode(p(1)) else selectedPerson
showScreenViewer(host)
case "SCREEN_SHARE_STOPPED" =>
if (screenViewerFrame != null) {
try screenViewerFrame.dispose() catch { case _: Throwable => }
screenViewerFrame = null
}
screenViewerLabel.setIcon(null)
screenViewerLabel.setText("Friend stopped screen sharing.")
case "SCREEN_FRAME" =>
if (p.length >= 3) {
renderScreenFrame(Util.decode(p(1)), p(2))
}
case "GROUP_CREATED" =>
if (
p.length >= 2
) {
JOptionPane.showMessageDialog(
frame,
"Permanent private group created:\n" +
Util.decode(
p(1)
),
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
client.send("FRIENDS_LIST")
}
case "GROUP_INFO" =>
if (p.length >= 4) {
val name = Util.decode(p(2))
val count = try p(3).toInt catch { case _: Throwable => 0 }
val out = new StringBuilder()
out.append("GROUP: ").append(name).append("\n\n")
var index = 4
var i = 0
while (i < count && index + 1 < p.length) {
val member = Util.decode(p(index))
val online = p(index + 1) == "1"
out.append(member)
.append(if (online) " ? ONLINE" else " ? OFFLINE")
.append("\n")
index += 2
i += 1
}
JOptionPane.showMessageDialog(
frame,
out.toString,
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
}
case "GROUP_UPDATED" =>
if (p.length >= 2) {
val id = Util.decode(p(1))
if (id == currentChat) {
chatTitle.setText(chatTitle.getText)
}
}
case "GROUP_LEFT" =>
if (p.length >= 2) {
val id = Util.decode(p(1))
if (id == currentChat) {
currentChat = ""
chatArea.setText("You left this group.")
chatTitle.setText("Select a chat")
}
client.send("FRIENDS_LIST")
}
case "REELS_LIST" =>
reelItems.clear()
if (reelsModelRef != null) {
reelsModelRef.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 + 5 < p.length
) {
val item =
ReelItem(
Util.decode(p(index)),
Util.decode(p(index + 1)),
Util.decode(p(index + 2)),
Util.decode(p(index + 3)),
Util.decode(p(index + 4)),
try {
p(index + 5).toLong
} catch {
case _: Throwable => 0L
}
)
reelItems.put(
item.fileId,
item
)
if (reelsModelRef != null) {
reelsModelRef.addElement(item)
}
index += 6
i += 1
}
}
case "MEDIA_AVAILABLE" =>
if (p.length >= 8) {
val chatId = Util.decode(p(1))
val fileId = Util.decode(p(2))
val sender = Util.decode(p(3))
val time = Util.decode(p(4))
val fileName = Util.decode(p(5))
val kind = Util.decode(p(6))
if (chatId == currentChat) {
appendChatLine(
"[" + time + "] " + sender + " sent " +
(if (kind == "video") "VIDEO: " else "IMAGE: ") +
fileName + "\n"
)
client.send(
"MEDIA_GET|" +
Util.encode(chatId) +
"|" +
Util.encode(fileId)
)
}
}
case "MEDIA_HISTORY" =>
if (p.length >= 3) {
val chatId = Util.decode(p(1))
val count = try p(2).toInt catch { case _: Throwable => 0 }
if (chatId == currentChat) {
var index = 3
var i = 0
while (i < count && index + 5 < p.length) {
val fileId = Util.decode(p(index))
val sender = Util.decode(p(index + 1))
val time = Util.decode(p(index + 2))
val fileName = Util.decode(p(index + 3))
val kind = Util.decode(p(index + 4))
val sizeText = p(index + 5)
appendChatLine(
"[" + time + "] " + sender + " - " +
(if (kind == "video") "VIDEO: " else "IMAGE: ") +
fileName + " (" + Util.humanSize(try sizeText.toLong catch { case _: Throwable => 0L }) + ")\n"
)
client.send(
"MEDIA_GET|" +
Util.encode(chatId) +
"|" +
Util.encode(fileId)
)
index += 6
i += 1
}
}
}
case "MEDIA_RECEIVED" =>
if (p.length >= 5) {
val chatId = Util.decode(p(1))
val path = Util.decode(p(2))
val fileName = Util.decode(p(3))
val kind = Util.decode(p(4))
val fileId = if (p.length >= 6) Util.decode(p(5)) else ""
if (
kind == "video" &&
fileId.nonEmpty &&
pendingReelFileIds.contains(fileId)
) {
pendingReelFileIds.remove(fileId)
Util.openMedia(Paths.get(path))
}
if (chatId == currentChat) {
appendChatLine(
"? Received " +
(if (kind == "video") "VIDEO: " else "IMAGE: " ) +
fileName +
"\nSaved: " +
path +
"\n"
)
val open =
JOptionPane.showConfirmDialog(
frame,
"Received: " + fileName + "\n\nOpen now?",
APP_NAME,
JOptionPane.YES_NO_OPTION
)
if (open == JOptionPane.YES_OPTION) {
Util.openFile(Paths.get(path))
}
}
}
case "MEDIA_SAVED" =>
()
case "FILE_SENT" =>
()
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" +
"? Automatic LAN discovery\n" +
"? Automatic subnet fallback discovery\n" +
"? Automatic remembered-server reconnect\n" +
"? Automatic Internet server connection when a public server is built into the deployment\n" +
"? 2-person direct chat\n" +
"? Multiple-person group chat\n" +
"? Username search\n" +
"? Online / offline status\n" +
"? Profile photo\n" +
"? SEND IMAGE / VIDEO TO PRIVATE CHAT OR GROUP\n" +
"? SCREEN RECORDING -> SEND AS ANIMATED MEDIA\n" +
"? ALL USERS A-Z + LIVE ONLINE STATUS\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" +
"No host/IP/port fields are shown to the user. Network selection is automatic."
)
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()