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