up version to ios 16
This commit is contained in:
parent
207187a439
commit
ea927d1e78
@ -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;
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -938,6 +938,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Не удалось отправить сообщение." : {
|
||||
|
||||
},
|
||||
"Не удалось подготовить данные запроса." : {
|
||||
"comment" : "Profile update encoding error"
|
||||
@ -1983,6 +1986,9 @@
|
||||
},
|
||||
"Сообщение" : {
|
||||
|
||||
},
|
||||
"Сообщение слишком длинное." : {
|
||||
|
||||
},
|
||||
"Сообщите о материалах" : {
|
||||
"comment" : "feedback category subtitle: content",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user