- Updated the application namespace and ID from "net.ypchat.app" to "de.ypchat.android" for better alignment with branding. - Increased Gradle heap size settings to optimize build performance. - Disabled dependency constraints to simplify dependency management. - Removed obsolete files related to the previous application structure, including MainActivity, YpChatApp, and various core components, streamlining the codebase. These changes collectively enhance the application's configuration and structure, improving maintainability and performance.
349 lines
13 KiB
Swift
349 lines
13 KiB
Swift
import Combine
|
|
import Foundation
|
|
import SocketIO
|
|
|
|
/// Entspricht `SocketClient.kt`: gleiche Events, `setSessionId`, `login`, Emits.
|
|
final class SocketClient: @unchecked Sendable {
|
|
private let baseURL: String
|
|
private var manager: SocketManager?
|
|
private var socket: SocketIOClient?
|
|
private var pendingExpressSessionId: String?
|
|
|
|
private let eventSubject = PassthroughSubject<SocketEvent, Never>()
|
|
|
|
/// Ereignisse vom Server (analog `SharedFlow<SocketEvent>`).
|
|
var eventPublisher: AnyPublisher<SocketEvent, Never> {
|
|
eventSubject.eraseToAnyPublisher()
|
|
}
|
|
|
|
var isConnected: Bool {
|
|
socket?.status == .connected
|
|
}
|
|
|
|
init(baseURL: String) {
|
|
self.baseURL = baseURL.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
}
|
|
|
|
func connect() {
|
|
disconnect()
|
|
|
|
guard let url = URL(string: baseURL) else {
|
|
notify(.error("Ungültige Socket-Basis-URL"))
|
|
return
|
|
}
|
|
|
|
var config: SocketIOClientConfiguration = [
|
|
.log(false),
|
|
.compress,
|
|
.reconnects(true),
|
|
.reconnectAttempts(-1),
|
|
.reconnectWait(1),
|
|
.reconnectWaitMax(30),
|
|
.version(.three),
|
|
]
|
|
|
|
if let cookies = HTTPCookieStorage.shared.cookies(for: url), !cookies.isEmpty {
|
|
config.insert(.cookies(cookies))
|
|
}
|
|
|
|
let manager = SocketManager(socketURL: url, config: config)
|
|
self.manager = manager
|
|
let socket = manager.defaultSocket
|
|
self.socket = socket
|
|
|
|
socket.on(clientEvent: .connect) { [weak self] _, _ in
|
|
self?.handleSocketConnect()
|
|
}
|
|
socket.on(clientEvent: .disconnect) { [weak self] data, _ in
|
|
let reason = data.first.map { String(describing: $0) }
|
|
self?.notify(.connectionChanged(connected: false, reason: reason))
|
|
}
|
|
socket.on(clientEvent: .error) { [weak self] data, _ in
|
|
let msg = data.first.map { String(describing: $0) } ?? ""
|
|
self?.notify(.error("Socket-Verbindung fehlgeschlagen: \(msg)"))
|
|
}
|
|
|
|
registerServerEvents(socket)
|
|
manager.connect()
|
|
}
|
|
|
|
func disconnect() {
|
|
socket?.removeAllHandlers()
|
|
manager?.disconnect()
|
|
socket = nil
|
|
manager = nil
|
|
}
|
|
|
|
func setSessionId(_ expressSessionId: String) {
|
|
pendingExpressSessionId = expressSessionId
|
|
guard isConnected else { return }
|
|
socket?.emit("setSessionId", ["expressSessionId": expressSessionId])
|
|
}
|
|
|
|
func login(userName: String, gender: String, age: Int, country: String, expressSessionId: String?) {
|
|
var payload: [String: Any] = [
|
|
"userName": userName,
|
|
"gender": gender,
|
|
"age": age,
|
|
"country": country,
|
|
]
|
|
if let expressSessionId {
|
|
payload["expressSessionId"] = expressSessionId
|
|
} else {
|
|
payload["expressSessionId"] = NSNull()
|
|
}
|
|
socket?.emit("login", payload)
|
|
}
|
|
|
|
func sendMessage(toUserName: String?, message: String, messageId: String = "\(Int(Date().timeIntervalSince1970 * 1000))") {
|
|
var payload: [String: Any] = [
|
|
"message": message.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
"messageId": messageId,
|
|
]
|
|
if let toUserName, !toUserName.isEmpty {
|
|
payload["toUserName"] = toUserName
|
|
}
|
|
socket?.emit("message", payload)
|
|
}
|
|
|
|
func sendImage(toUserName: String, imageCode: String, imageUrl: String, messageId: String = "\(Int(Date().timeIntervalSince1970 * 1000))") {
|
|
socket?.emit(
|
|
"message",
|
|
[
|
|
"toUserName": toUserName,
|
|
"message": imageCode,
|
|
"messageId": messageId,
|
|
"isImage": true,
|
|
"imageUrl": imageUrl,
|
|
]
|
|
)
|
|
}
|
|
|
|
func requestConversation(withUserName: String) {
|
|
socket?.emit("requestConversation", ["withUserName": withUserName])
|
|
}
|
|
|
|
func userSearch(nameIncludes: String?, minAge: Int?, maxAge: Int?, countries: [String], genders: [String]) {
|
|
var payload: [String: Any] = [
|
|
"countries": countries,
|
|
"genders": genders,
|
|
]
|
|
payload["nameIncludes"] = nameIncludes ?? NSNull()
|
|
payload["minAge"] = minAge.map { $0 as Any } ?? NSNull()
|
|
payload["maxAge"] = maxAge.map { $0 as Any } ?? NSNull()
|
|
socket?.emit("userSearch", payload)
|
|
}
|
|
|
|
func requestHistory() {
|
|
socket?.emit("requestHistory")
|
|
}
|
|
|
|
func requestOpenConversations() {
|
|
socket?.emit("requestOpenConversations")
|
|
}
|
|
|
|
func blockUser(userName: String) {
|
|
socket?.emit("blockUser", ["userName": userName])
|
|
}
|
|
|
|
func unblockUser(userName: String) {
|
|
socket?.emit("unblockUser", ["userName": userName])
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func handleSocketConnect() {
|
|
if let sid = pendingExpressSessionId {
|
|
socket?.emit("setSessionId", ["expressSessionId": sid])
|
|
}
|
|
// Reihenfolge wie `SocketClient.kt` (EVENT_CONNECT): zuerst setSessionId, dann ConnectionChanged.
|
|
notify(.connectionChanged(connected: true, reason: nil))
|
|
}
|
|
|
|
private func notify(_ event: SocketEvent) {
|
|
eventSubject.send(event)
|
|
}
|
|
|
|
private func registerServerEvents(_ socket: SocketIOClient) {
|
|
socket.on("connected") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
self?.notify(
|
|
.connected(
|
|
sessionId: json.stringOrNil("sessionId"),
|
|
loggedIn: json.bool("loggedIn", default: false),
|
|
user: json.userObject("user")
|
|
)
|
|
)
|
|
}
|
|
socket.on("loginSuccess") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
self?.notify(.loginSuccess(sessionId: json.stringOrNil("sessionId"), user: json.userObject("user")))
|
|
}
|
|
socket.on("userList") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data),
|
|
let users = json["users"] as? [[String: Any]]
|
|
else { return }
|
|
self?.notify(.userList(users: users.map { UserDto(json: $0) }))
|
|
}
|
|
socket.on("message") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
self?.notify(.incomingMessage(ChatMessageDto(json: json)))
|
|
}
|
|
socket.on("messageSent") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
self?.notify(.messageSent(messageId: json.stringOrNil("messageId"), to: json.stringOrNil("to")))
|
|
}
|
|
socket.on("conversation") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
let withName = json.string("with")
|
|
let rawMessages = json["messages"] as? [[String: Any]] ?? []
|
|
let messages = rawMessages.map { ChatMessageDto(json: $0) }
|
|
self?.notify(.conversation(withUserName: withName, messages: messages))
|
|
}
|
|
socket.on("searchResults") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data),
|
|
let results = json["results"] as? [[String: Any]]
|
|
else { return }
|
|
self?.notify(.searchResults(results.map { UserDto(json: $0) }))
|
|
}
|
|
socket.on("historyResults") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data),
|
|
let results = json["results"] as? [[String: Any]]
|
|
else { return }
|
|
self?.notify(.historyResults(results.map { HistoryItemDto(json: $0) }))
|
|
}
|
|
socket.on("inboxResults") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data),
|
|
let results = json["results"] as? [[String: Any]]
|
|
else { return }
|
|
self?.notify(.inboxResults(results.map { InboxItemDto(json: $0) }))
|
|
}
|
|
socket.on("unreadChats") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
self?.notify(.unreadChats(count: json.int("count", default: 0)))
|
|
}
|
|
socket.on("userBlocked") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
self?.notify(.userBlocked(json.string("userName")))
|
|
}
|
|
socket.on("userUnblocked") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
self?.notify(.userUnblocked(json.string("userName")))
|
|
}
|
|
socket.on("commandResult") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
let lines = (json["lines"] as? [Any])?.map { String(describing: $0) } ?? []
|
|
let kind = json.string("kind", default: "info")
|
|
self?.notify(.commandResult(lines: lines, kind: kind))
|
|
}
|
|
socket.on("commandTable") { [weak self] data, _ in
|
|
guard let json = Self.firstDictionary(from: data) else { return }
|
|
let title = json.string("title", default: "Ausgabe")
|
|
let columns = (json["columns"] as? [Any])?.map { String(describing: $0) } ?? []
|
|
let rows = Self.nestedStringRows(json["rows"])
|
|
self?.notify(.commandTable(title: title, columns: columns, rows: rows))
|
|
}
|
|
socket.on("error") { [weak self] data, _ in
|
|
let json = Self.firstDictionary(from: data)
|
|
let message =
|
|
json?.stringOrNil("message")
|
|
?? data.first.map { String(describing: $0) }
|
|
?? "Unbekannter Socket-Fehler"
|
|
self?.notify(.error(message))
|
|
}
|
|
}
|
|
|
|
private static func firstDictionary(from data: [Any]) -> [String: Any]? {
|
|
guard let first = data.first else { return nil }
|
|
if let d = first as? [String: Any] { return d }
|
|
if let n = first as? NSDictionary { return n as? [String: Any] }
|
|
return nil
|
|
}
|
|
|
|
private static func nestedStringRows(_ any: Any?) -> [[String]] {
|
|
guard let outer = any as? [Any] else { return [] }
|
|
return outer.map { row in
|
|
(row as? [Any])?.map { String(describing: $0) } ?? []
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - JSON helpers (JSONObject-Äquivalent)
|
|
|
|
private extension Dictionary where Key == String, Value == Any {
|
|
func stringOrNil(_ key: String) -> String? {
|
|
guard let v = self[key], !(v is NSNull) else { return nil }
|
|
if let s = v as? String { return s }
|
|
return String(describing: v)
|
|
}
|
|
|
|
func string(_ key: String, default def: String = "") -> String {
|
|
stringOrNil(key) ?? def
|
|
}
|
|
|
|
func bool(_ key: String, default def: Bool) -> Bool {
|
|
guard let v = self[key], !(v is NSNull) else { return def }
|
|
if let b = v as? Bool { return b }
|
|
if let n = v as? NSNumber { return n.boolValue }
|
|
return def
|
|
}
|
|
|
|
func int(_ key: String, default def: Int) -> Int {
|
|
guard let v = self[key], !(v is NSNull) else { return def }
|
|
if let i = v as? Int { return i }
|
|
if let n = v as? NSNumber { return n.intValue }
|
|
return def
|
|
}
|
|
|
|
func userObject(_ key: String) -> UserDto? {
|
|
guard let nested = self[key] as? [String: Any] else { return nil }
|
|
return UserDto(json: nested)
|
|
}
|
|
}
|
|
|
|
private extension UserDto {
|
|
init(json: [String: Any]) {
|
|
self.init(
|
|
sessionId: json.stringOrNil("sessionId"),
|
|
userName: json.string("userName"),
|
|
gender: json.string("gender"),
|
|
age: json.int("age", default: 0),
|
|
country: json.string("country"),
|
|
isoCountryCode: json.string("isoCountryCode")
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension ChatMessageDto {
|
|
init(json: [String: Any]) {
|
|
self.init(
|
|
from: json.string("from"),
|
|
to: json.stringOrNil("to"),
|
|
message: json.string("message"),
|
|
messageId: json.stringOrNil("messageId"),
|
|
timestamp: json.string("timestamp"),
|
|
read: json.bool("read", default: false),
|
|
isImage: json.bool("isImage", default: false),
|
|
imageType: json.stringOrNil("imageType"),
|
|
imageUrl: json.stringOrNil("imageUrl"),
|
|
imageCode: json.stringOrNil("imageCode")
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension HistoryItemDto {
|
|
init(json: [String: Any]) {
|
|
let last: ChatMessageDto? = {
|
|
guard let nested = json["lastMessage"] as? [String: Any] else { return nil }
|
|
return ChatMessageDto(json: nested)
|
|
}()
|
|
self.init(userName: json.string("userName"), lastMessage: last)
|
|
}
|
|
}
|
|
|
|
private extension InboxItemDto {
|
|
init(json: [String: Any]) {
|
|
self.init(userName: json.string("userName"), unreadCount: json.int("unreadCount", default: 0))
|
|
}
|
|
}
|