up version to ios 16

This commit is contained in:
cheykrym 2025-10-08 06:10:37 +03:00
parent 207187a439
commit ea927d1e78
6 changed files with 209 additions and 8 deletions

View File

@ -411,10 +411,10 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 16;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 11.5;
MACOSX_DEPLOYMENT_TARGET = 11;
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = org.yobble.yobble;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -424,7 +424,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 1.3;
XROS_DEPLOYMENT_TARGET = 1;
};
name = Debug;
};
@ -451,10 +451,10 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 16;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 11.5;
MACOSX_DEPLOYMENT_TARGET = 11;
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = org.yobble.yobble;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -464,7 +464,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 1.3;
XROS_DEPLOYMENT_TARGET = 1;
};
name = Release;
};

View File

@ -10,6 +10,25 @@ struct PrivateChatHistoryData: Decodable {
let hasMore: Bool
}
struct PrivateMessageSendData: Decodable {
let messageId: String
let chatId: String
let createdAt: Date?
private enum CodingKeys: String, CodingKey {
case messageId
case chatId
case createdAt
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.messageId = try container.decodeFlexibleString(forKey: .messageId)
self.chatId = try container.decodeFlexibleString(forKey: .chatId)
self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt)
}
}
struct PrivateChatListItem: Decodable, Identifiable {
enum ChatType: String, Decodable {
case `self`
@ -76,6 +95,32 @@ struct MessageItem: Decodable, Identifiable {
self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt)
self.forwardMetadata = try container.decodeIfPresent(ForwardMetadata.self, forKey: .forwardMetadata)
}
init(
messageId: String,
messageType: String,
chatId: String,
senderId: String,
senderData: ChatProfile?,
content: String?,
mediaLink: String?,
isViewed: Bool?,
createdAt: Date?,
updatedAt: Date?,
forwardMetadata: ForwardMetadata?
) {
self.messageId = messageId
self.messageType = messageType
self.chatId = chatId
self.senderId = senderId
self.senderData = senderData
self.content = content
self.mediaLink = mediaLink
self.isViewed = isViewed
self.createdAt = createdAt
self.updatedAt = updatedAt
self.forwardMetadata = forwardMetadata
}
}
struct ForwardMetadata: Decodable {

View File

@ -111,6 +111,52 @@ final class ChatService {
}
}
func sendPrivateMessage(
chatId: String,
content: String,
completion: @escaping (Result<PrivateMessageSendData, Error>) -> Void
) {
let payload: [String: Any] = [
"chat_id": chatId,
"content": content,
"message_type": ["text"]
]
guard let body = try? JSONSerialization.data(withJSONObject: payload, options: []) else {
completion(.failure(ChatServiceError.decoding(debugDescription: "Невозможно сериализовать тело запроса.")))
return
}
client.request(
path: "/v1/chat/private/send",
method: .post,
headers: ["Content-Type": "application/json"],
body: body,
requiresAuth: true
) { [decoder] result in
switch result {
case .success(let response):
do {
let apiResponse = try decoder.decode(APIResponse<PrivateMessageSendData>.self, from: response.data)
guard apiResponse.status == "fine" else {
let message = apiResponse.detail ?? NSLocalizedString("Не удалось отправить сообщение.", comment: "")
completion(.failure(ChatServiceError.unexpectedStatus(message)))
return
}
completion(.success(apiResponse.data))
} catch {
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
if AppConfig.DEBUG {
print("[ChatService] send private message decode failed: \(debugMessage)")
}
completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
}
case .failure(let error):
completion(.failure(error))
}
}
}
private static func decodeDate(from decoder: Decoder) throws -> Date {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)

View File

@ -938,6 +938,9 @@
}
}
}
},
"Не удалось отправить сообщение." : {
},
"Не удалось подготовить данные запроса." : {
"comment" : "Profile update encoding error"
@ -1983,6 +1986,9 @@
},
"Сообщение" : {
},
"Сообщение слишком длинное." : {
},
"Сообщите о материалах" : {
"comment" : "feedback category subtitle: content",

View File

@ -6,15 +6,19 @@ final class PrivateChatViewModel: ObservableObject {
@Published private(set) var isInitialLoading: Bool = false
@Published private(set) var isLoadingMore: Bool = false
@Published var errorMessage: String?
@Published private(set) var isSending: Bool = false
private let chatService: ChatService
private let chatId: String
private let currentUserId: String?
private let pageSize: Int
private let maxMessageLength: Int = 4096
private var hasMore: Bool = true
private var didLoadInitially: Bool = false
init(chatId: String, chatService: ChatService = ChatService(), pageSize: Int = 30) {
init(chatId: String, currentUserId: String?, chatService: ChatService = ChatService(), pageSize: Int = 30) {
self.chatId = chatId
self.currentUserId = currentUserId
self.chatService = chatService
self.pageSize = pageSize
}
@ -43,6 +47,59 @@ final class PrivateChatViewModel: ObservableObject {
}
}
func sendMessage(text: String, completion: @escaping (Bool) -> Void) {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
completion(false)
return
}
guard trimmed.count <= maxMessageLength else {
errorMessage = NSLocalizedString("Сообщение слишком длинное.", comment: "")
completion(false)
return
}
guard !isSending else {
completion(false)
return
}
guard let currentUserId else {
completion(false)
return
}
isSending = true
chatService.sendPrivateMessage(chatId: chatId, content: trimmed) { [weak self] result in
guard let self else { return }
switch result {
case .success(let data):
let newMessage = MessageItem(
messageId: data.messageId,
messageType: "text",
chatId: data.chatId,
senderId: currentUserId,
senderData: nil,
content: trimmed,
mediaLink: nil,
isViewed: true,
createdAt: data.createdAt,
updatedAt: data.createdAt,
forwardMetadata: nil
)
self.messages = Self.merge(existing: self.messages, newMessages: [newMessage])
self.errorMessage = nil
completion(true)
case .failure(let error):
self.errorMessage = self.message(for: error)
completion(false)
}
self.isSending = false
}
}
func refresh() {
didLoadInitially = false
loadInitialHistory(force: true)

View File

@ -6,11 +6,13 @@ struct PrivateChatView: View {
@StateObject private var viewModel: PrivateChatViewModel
@State private var hasPositionedToBottom: Bool = false
@State private var draftText: String = ""
@FocusState private var isComposerFocused: Bool
init(chat: PrivateChatListItem, currentUserId: String?) {
self.chat = chat
self.currentUserId = currentUserId
_viewModel = StateObject(wrappedValue: PrivateChatViewModel(chatId: chat.chatId))
_viewModel = StateObject(wrappedValue: PrivateChatViewModel(chatId: chat.chatId, currentUserId: currentUserId))
}
var body: some View {
@ -37,6 +39,9 @@ struct PrivateChatView: View {
hasPositionedToBottom = false
}
}
.safeAreaInset(edge: .bottom) {
composer
}
}
@ViewBuilder
@ -185,6 +190,48 @@ struct PrivateChatView: View {
.padding(.top, 8)
}
private var composer: some View {
VStack(spacing: 0) {
Divider()
HStack(alignment: .center, spacing: 12) {
TextField(NSLocalizedString("Сообщение", comment: ""), text: $draftText, axis: .vertical)
.lineLimit(1...4)
.focused($isComposerFocused)
.submitLabel(.send)
.disabled(viewModel.isSending || currentUserId == nil)
.onSubmit { sendCurrentMessage() }
Button(action: sendCurrentMessage) {
Image(systemName: viewModel.isSending ? "hourglass" : "paperplane.fill")
.font(.system(size: 18, weight: .semibold))
}
.disabled(isSendDisabled)
.buttonStyle(.plain)
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background(.clear)
}
.background(.ultraThinMaterial)
}
private var isSendDisabled: Bool {
draftText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending || currentUserId == nil
}
private func sendCurrentMessage() {
let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return }
viewModel.sendMessage(text: text) { success in
if success {
draftText = ""
hasPositionedToBottom = true
}
}
}
private static let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none