Refactor application structure and configuration
- 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.
This commit is contained in:
493
ios/YpChat/Data/ChatRepository.swift
Normal file
493
ios/YpChat/Data/ChatRepository.swift
Normal file
@@ -0,0 +1,493 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
/// Entspricht `CommandTableState` in `ChatRepository.kt`.
|
||||
struct CommandTableState: Equatable, Sendable {
|
||||
var title: String
|
||||
var columns: [String]
|
||||
var rows: [[String]]
|
||||
}
|
||||
|
||||
/// Zentraler UI-Zustand wie `ChatState` in `ChatRepository.kt`.
|
||||
struct ChatState: Equatable, Sendable {
|
||||
var isConnected: Bool = false
|
||||
var isLoggedIn: Bool = false
|
||||
var expressSessionId: String?
|
||||
var currentUser: UserDto?
|
||||
var users: [UserDto] = []
|
||||
var currentConversation: String?
|
||||
var messages: [ChatMessageDto] = []
|
||||
var searchResults: [UserDto] = []
|
||||
var inboxResults: [InboxItemDto] = []
|
||||
var historyResults: [HistoryItemDto] = []
|
||||
var countries: [CountryOption] = []
|
||||
var feedbackItems: [FeedbackItemDto] = []
|
||||
var feedbackMessage: String?
|
||||
var feedbackAdminAuthenticated: Bool = false
|
||||
var feedbackAdminUserName: String?
|
||||
var feedbackAdminError: String?
|
||||
var partnerLinks: [PartnerLinkDto] = []
|
||||
var partnersError: String?
|
||||
var savedProfile: SavedProfile = SavedProfile()
|
||||
var commandLines: [String] = []
|
||||
var commandKind: String?
|
||||
var commandTable: CommandTableState?
|
||||
var awaitingLoginUsername: Bool = false
|
||||
var awaitingLoginPassword: Bool = false
|
||||
var remainingSecondsToTimeout: Int = 1800
|
||||
var isUploadingImage: Bool = false
|
||||
var imageUploadMessage: String?
|
||||
var unreadChatsCount: Int = 0
|
||||
var errorMessage: String?
|
||||
}
|
||||
|
||||
/// Analog `ChatRepository.kt`: REST + Socket + `reduce` + Timeout.
|
||||
final class ChatRepository: ObservableObject {
|
||||
@Published private(set) var state = ChatState()
|
||||
|
||||
private let api: RestAPIClient
|
||||
private let socket: SocketClient
|
||||
private let profileStore: ProfileStore
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var timeoutTickerStarted = false
|
||||
|
||||
init(api: RestAPIClient, socket: SocketClient, profileStore: ProfileStore) {
|
||||
self.api = api
|
||||
self.socket = socket
|
||||
self.profileStore = profileStore
|
||||
|
||||
socket.eventPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] event in
|
||||
self?.reduce(event)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
startTimeoutTicker()
|
||||
}
|
||||
|
||||
// MARK: - Session / Profil
|
||||
|
||||
func restoreSession() async {
|
||||
var s = state
|
||||
s.savedProfile = profileStore.read()
|
||||
state = s
|
||||
|
||||
await loadCountries()
|
||||
await loadFeedbackAdminStatus()
|
||||
|
||||
do {
|
||||
let session = try await api.sessionStatus()
|
||||
var next = state
|
||||
next.expressSessionId = session.sessionId
|
||||
next.isLoggedIn = session.loggedIn && session.user != nil
|
||||
next.currentUser = session.user
|
||||
next.errorMessage = nil
|
||||
state = next
|
||||
if session.loggedIn, session.user != nil {
|
||||
resetTimeout()
|
||||
}
|
||||
connectSocket(expressSessionId: session.sessionId)
|
||||
} catch {
|
||||
var next = state
|
||||
next.errorMessage = error.localizedDescription
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
func loadCountries() async {
|
||||
do {
|
||||
let countries = try await api.countries()
|
||||
let locale = Locale.current
|
||||
let options: [CountryOption] = countries.map { englishName, code in
|
||||
let normalized = code.uppercased()
|
||||
let localized = Locale.current.localizedString(forRegionCode: normalized)
|
||||
let display = localized.flatMap { $0.isEmpty ? nil : $0 } ?? englishName
|
||||
return CountryOption(englishName: englishName, displayName: display, isoCode: code)
|
||||
}
|
||||
.sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }
|
||||
|
||||
var next = state
|
||||
next.countries = options
|
||||
state = next
|
||||
} catch {
|
||||
var next = state
|
||||
next.errorMessage = "Country list could not be loaded: \(error.localizedDescription)"
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
func login(userName: String, gender: String, age: Int, country: String) async {
|
||||
profileStore.write(SavedProfile(nickname: userName, gender: gender, age: age, country: country))
|
||||
|
||||
let session = try? await api.sessionStatus()
|
||||
let sessionId = session?.sessionId ?? state.expressSessionId
|
||||
|
||||
var next = state
|
||||
next.expressSessionId = sessionId
|
||||
state = next
|
||||
|
||||
if !socket.isConnected {
|
||||
connectSocket(expressSessionId: sessionId)
|
||||
}
|
||||
socket.login(userName: userName, gender: gender, age: age, country: country, expressSessionId: sessionId)
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
try? await api.logout()
|
||||
socket.disconnect()
|
||||
clearCookies()
|
||||
var fresh = ChatState()
|
||||
fresh.savedProfile = profileStore.read()
|
||||
fresh.countries = state.countries
|
||||
state = fresh
|
||||
}
|
||||
|
||||
func connectSocket(expressSessionId: String? = nil) {
|
||||
let sid = expressSessionId ?? state.expressSessionId
|
||||
socket.connect()
|
||||
if let sid {
|
||||
socket.setSessionId(sid)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Chat
|
||||
|
||||
func openConversation(userName: String) {
|
||||
var next = state
|
||||
next.currentConversation = userName
|
||||
next.messages = []
|
||||
state = next
|
||||
socket.requestConversation(withUserName: userName)
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func closeConversation() {
|
||||
var next = state
|
||||
next.currentConversation = nil
|
||||
next.messages = []
|
||||
state = next
|
||||
}
|
||||
|
||||
func sendMessage(text: String) {
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
let target = state.currentConversation
|
||||
let isCommand = trimmed.hasPrefix("/")
|
||||
if target == nil, !isCommand { return }
|
||||
|
||||
socket.sendMessage(toUserName: target, message: trimmed)
|
||||
if !isCommand {
|
||||
let msg = ChatMessageDto(
|
||||
from: state.currentUser?.userName ?? "",
|
||||
to: target,
|
||||
message: trimmed,
|
||||
messageId: nil,
|
||||
timestamp: ISO8601DateFormatter().string(from: Date()),
|
||||
read: false,
|
||||
isImage: false,
|
||||
imageType: nil,
|
||||
imageUrl: nil,
|
||||
imageCode: nil
|
||||
)
|
||||
var next = state
|
||||
next.messages.append(msg)
|
||||
state = next
|
||||
}
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func sendImage(toUserName: String, imageCode: String, imageUrl: String) {
|
||||
let absoluteUrl: String =
|
||||
imageUrl.hasPrefix("http") ? imageUrl : AppConfig.baseURL + imageUrl
|
||||
socket.sendImage(toUserName: toUserName, imageCode: imageCode, imageUrl: absoluteUrl)
|
||||
let msg = ChatMessageDto(
|
||||
from: state.currentUser?.userName ?? "",
|
||||
to: toUserName,
|
||||
message: absoluteUrl,
|
||||
messageId: nil,
|
||||
timestamp: ISO8601DateFormatter().string(from: Date()),
|
||||
read: false,
|
||||
isImage: true,
|
||||
imageType: nil,
|
||||
imageUrl: absoluteUrl,
|
||||
imageCode: imageCode
|
||||
)
|
||||
var next = state
|
||||
next.messages.append(msg)
|
||||
state = next
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func setImageUploadState(inProgress: Bool, message: String? = nil) {
|
||||
var next = state
|
||||
next.isUploadingImage = inProgress
|
||||
next.imageUploadMessage = message
|
||||
state = next
|
||||
}
|
||||
|
||||
func uploadImage(data: Data, fileName: String, mimeType: String) async throws -> ImageUploadResponse {
|
||||
let result = try await api.uploadImage(data: data, fileName: fileName, mimeType: mimeType)
|
||||
return result.response
|
||||
}
|
||||
|
||||
// MARK: - Feedback / Partner
|
||||
|
||||
func loadFeedback() async {
|
||||
do {
|
||||
let response = try await api.feedback()
|
||||
var next = state
|
||||
next.feedbackItems = response.items
|
||||
next.feedbackMessage = nil
|
||||
state = next
|
||||
} catch {
|
||||
var next = state
|
||||
next.feedbackMessage = error.localizedDescription
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
func submitFeedback(comment: String) async {
|
||||
let trimmed = comment.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
|
||||
let profile = state.savedProfile
|
||||
do {
|
||||
try await api.submitFeedback(
|
||||
FeedbackRequest(
|
||||
name: profile.nickname,
|
||||
age: profile.age,
|
||||
country: profile.country,
|
||||
gender: profile.gender,
|
||||
comment: trimmed
|
||||
)
|
||||
)
|
||||
var next = state
|
||||
next.feedbackMessage = "Feedback saved"
|
||||
state = next
|
||||
await loadFeedback()
|
||||
} catch {
|
||||
var next = state
|
||||
next.feedbackMessage = error.localizedDescription
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
func loadFeedbackAdminStatus() async {
|
||||
do {
|
||||
let response = try await api.feedbackAdminStatus()
|
||||
var next = state
|
||||
next.feedbackAdminAuthenticated = response.authenticated
|
||||
next.feedbackAdminUserName = response.username
|
||||
next.feedbackAdminError = nil
|
||||
state = next
|
||||
} catch {
|
||||
var next = state
|
||||
next.feedbackAdminAuthenticated = false
|
||||
next.feedbackAdminUserName = nil
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
func loginFeedbackAdmin(username: String, password: String) async {
|
||||
do {
|
||||
let response = try await api.feedbackAdminLogin(FeedbackAdminLoginRequest(username: username, password: password))
|
||||
var next = state
|
||||
next.feedbackAdminAuthenticated = true
|
||||
next.feedbackAdminUserName = response.username
|
||||
next.feedbackAdminError = nil
|
||||
state = next
|
||||
await loadFeedback()
|
||||
} catch {
|
||||
var next = state
|
||||
next.feedbackAdminError = error.localizedDescription
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
func logoutFeedbackAdmin() async {
|
||||
try? await api.feedbackAdminLogout()
|
||||
var next = state
|
||||
next.feedbackAdminAuthenticated = false
|
||||
next.feedbackAdminUserName = nil
|
||||
next.feedbackAdminError = nil
|
||||
state = next
|
||||
}
|
||||
|
||||
func deleteFeedback(id: String) async {
|
||||
do {
|
||||
try await api.deleteFeedback(id: id)
|
||||
await loadFeedback()
|
||||
} catch {
|
||||
var next = state
|
||||
next.feedbackAdminError = error.localizedDescription
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
func loadPartners() async {
|
||||
do {
|
||||
let links = try await api.partners()
|
||||
var next = state
|
||||
next.partnerLinks = links
|
||||
next.partnersError = nil
|
||||
state = next
|
||||
} catch {
|
||||
var next = state
|
||||
next.partnersError = error.localizedDescription
|
||||
state = next
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Socket actions
|
||||
|
||||
func search(nameIncludes: String?, minAge: Int?, maxAge: Int?, countries: [String], genders: [String]) {
|
||||
socket.userSearch(nameIncludes: nameIncludes, minAge: minAge, maxAge: maxAge, countries: countries, genders: genders)
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func requestInbox() {
|
||||
socket.requestOpenConversations()
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func requestHistory() {
|
||||
socket.requestHistory()
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func blockUser(userName: String) {
|
||||
socket.blockUser(userName: userName)
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
func unblockUser(userName: String) {
|
||||
socket.unblockUser(userName: userName)
|
||||
resetTimeout()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func clearCookies() {
|
||||
HTTPCookieStorage.shared.cookies?.forEach { HTTPCookieStorage.shared.deleteCookie($0) }
|
||||
}
|
||||
|
||||
private func startTimeoutTicker() {
|
||||
guard !timeoutTickerStarted else { return }
|
||||
timeoutTickerStarted = true
|
||||
Timer.publish(every: 1, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
self?.runTimeoutTick()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func runTimeoutTick() {
|
||||
guard state.isLoggedIn else { return }
|
||||
let nextSec = max(0, state.remainingSecondsToTimeout - 1)
|
||||
var next = state
|
||||
next.remainingSecondsToTimeout = nextSec
|
||||
state = next
|
||||
if nextSec == 0 {
|
||||
Task { await self.logout() }
|
||||
}
|
||||
}
|
||||
|
||||
private func resetTimeout() {
|
||||
guard state.isLoggedIn || state.currentUser != nil else { return }
|
||||
var next = state
|
||||
next.remainingSecondsToTimeout = 1800
|
||||
state = next
|
||||
}
|
||||
|
||||
private func reduce(_ event: SocketEvent) {
|
||||
state = Self.apply(state: state, event: event, socket: socket)
|
||||
}
|
||||
|
||||
private static func apply(state: ChatState, event: SocketEvent, socket: SocketClient) -> ChatState {
|
||||
var current = state
|
||||
switch event {
|
||||
case .connectionChanged(let connected, _):
|
||||
current.isConnected = connected
|
||||
|
||||
case .connected(let sessionId, let loggedIn, let user):
|
||||
if let sid = sessionId {
|
||||
socket.setSessionId(sid)
|
||||
}
|
||||
current.expressSessionId = sessionId ?? current.expressSessionId
|
||||
current.isLoggedIn = loggedIn || current.isLoggedIn
|
||||
current.currentUser = user ?? current.currentUser
|
||||
current.errorMessage = nil
|
||||
if current.isLoggedIn {
|
||||
current.remainingSecondsToTimeout = 1800
|
||||
}
|
||||
|
||||
case .loginSuccess(let sessionId, let user):
|
||||
current.expressSessionId = sessionId ?? current.expressSessionId
|
||||
current.isLoggedIn = true
|
||||
current.currentUser = user
|
||||
current.errorMessage = nil
|
||||
current.remainingSecondsToTimeout = 1800
|
||||
|
||||
case .userList(let users):
|
||||
current.users = users
|
||||
|
||||
case .incomingMessage(let message):
|
||||
let active = current.currentConversation == message.from
|
||||
if active {
|
||||
current.messages.append(message)
|
||||
}
|
||||
if !active {
|
||||
current.unreadChatsCount += 1
|
||||
}
|
||||
current.remainingSecondsToTimeout = 1800
|
||||
|
||||
case .messageSent:
|
||||
break
|
||||
|
||||
case .conversation(let withUserName, let messages):
|
||||
current.currentConversation = withUserName
|
||||
current.messages = messages
|
||||
current.unreadChatsCount = max(0, current.unreadChatsCount - 1)
|
||||
current.remainingSecondsToTimeout = 1800
|
||||
|
||||
case .searchResults(let results):
|
||||
current.searchResults = results
|
||||
|
||||
case .historyResults(let results):
|
||||
current.historyResults = results
|
||||
|
||||
case .inboxResults(let results):
|
||||
current.inboxResults = results
|
||||
|
||||
case .unreadChats(let count):
|
||||
current.unreadChatsCount = count
|
||||
|
||||
case .userBlocked(let userName):
|
||||
current.errorMessage = "\(userName) blocked"
|
||||
|
||||
case .userUnblocked(let userName):
|
||||
current.errorMessage = "\(userName) unblocked"
|
||||
|
||||
case .commandResult(let lines, let kind):
|
||||
current.commandLines = lines
|
||||
current.commandKind = kind
|
||||
current.commandTable = nil
|
||||
current.awaitingLoginUsername = kind == "loginPromptUsername"
|
||||
current.awaitingLoginPassword = kind == "loginPromptPassword"
|
||||
if kind == "info" || kind.hasPrefix("login") {
|
||||
current.errorMessage = lines.joined(separator: " | ")
|
||||
}
|
||||
|
||||
case .commandTable(let title, let columns, let rows):
|
||||
current.commandTable = CommandTableState(title: title, columns: columns, rows: rows)
|
||||
|
||||
case .error(let message):
|
||||
current.errorMessage = message
|
||||
}
|
||||
return current
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user