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_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait 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 = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@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;
|
MARKETING_VERSION = 0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.yobble.yobble;
|
PRODUCT_BUNDLE_IDENTIFIER = org.yobble.yobble;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@ -424,7 +424,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
XROS_DEPLOYMENT_TARGET = 1.3;
|
XROS_DEPLOYMENT_TARGET = 1;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -451,10 +451,10 @@
|
|||||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait 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 = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@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;
|
MARKETING_VERSION = 0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.yobble.yobble;
|
PRODUCT_BUNDLE_IDENTIFIER = org.yobble.yobble;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@ -464,7 +464,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
XROS_DEPLOYMENT_TARGET = 1.3;
|
XROS_DEPLOYMENT_TARGET = 1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,25 @@ struct PrivateChatHistoryData: Decodable {
|
|||||||
let hasMore: Bool
|
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 {
|
struct PrivateChatListItem: Decodable, Identifiable {
|
||||||
enum ChatType: String, Decodable {
|
enum ChatType: String, Decodable {
|
||||||
case `self`
|
case `self`
|
||||||
@ -76,6 +95,32 @@ struct MessageItem: Decodable, Identifiable {
|
|||||||
self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt)
|
self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt)
|
||||||
self.forwardMetadata = try container.decodeIfPresent(ForwardMetadata.self, forKey: .forwardMetadata)
|
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 {
|
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 {
|
private static func decodeDate(from decoder: Decoder) throws -> Date {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let string = try container.decode(String.self)
|
let string = try container.decode(String.self)
|
||||||
|
|||||||
@ -938,6 +938,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Не удалось отправить сообщение." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Не удалось подготовить данные запроса." : {
|
"Не удалось подготовить данные запроса." : {
|
||||||
"comment" : "Profile update encoding error"
|
"comment" : "Profile update encoding error"
|
||||||
@ -1983,6 +1986,9 @@
|
|||||||
},
|
},
|
||||||
"Сообщение" : {
|
"Сообщение" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Сообщение слишком длинное." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Сообщите о материалах" : {
|
"Сообщите о материалах" : {
|
||||||
"comment" : "feedback category subtitle: content",
|
"comment" : "feedback category subtitle: content",
|
||||||
|
|||||||
@ -6,15 +6,19 @@ final class PrivateChatViewModel: ObservableObject {
|
|||||||
@Published private(set) var isInitialLoading: Bool = false
|
@Published private(set) var isInitialLoading: Bool = false
|
||||||
@Published private(set) var isLoadingMore: Bool = false
|
@Published private(set) var isLoadingMore: Bool = false
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
|
@Published private(set) var isSending: Bool = false
|
||||||
|
|
||||||
private let chatService: ChatService
|
private let chatService: ChatService
|
||||||
private let chatId: String
|
private let chatId: String
|
||||||
|
private let currentUserId: String?
|
||||||
private let pageSize: Int
|
private let pageSize: Int
|
||||||
|
private let maxMessageLength: Int = 4096
|
||||||
private var hasMore: Bool = true
|
private var hasMore: Bool = true
|
||||||
private var didLoadInitially: Bool = false
|
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.chatId = chatId
|
||||||
|
self.currentUserId = currentUserId
|
||||||
self.chatService = chatService
|
self.chatService = chatService
|
||||||
self.pageSize = pageSize
|
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() {
|
func refresh() {
|
||||||
didLoadInitially = false
|
didLoadInitially = false
|
||||||
loadInitialHistory(force: true)
|
loadInitialHistory(force: true)
|
||||||
|
|||||||
@ -6,11 +6,13 @@ struct PrivateChatView: View {
|
|||||||
|
|
||||||
@StateObject private var viewModel: PrivateChatViewModel
|
@StateObject private var viewModel: PrivateChatViewModel
|
||||||
@State private var hasPositionedToBottom: Bool = false
|
@State private var hasPositionedToBottom: Bool = false
|
||||||
|
@State private var draftText: String = ""
|
||||||
|
@FocusState private var isComposerFocused: Bool
|
||||||
|
|
||||||
init(chat: PrivateChatListItem, currentUserId: String?) {
|
init(chat: PrivateChatListItem, currentUserId: String?) {
|
||||||
self.chat = chat
|
self.chat = chat
|
||||||
self.currentUserId = currentUserId
|
self.currentUserId = currentUserId
|
||||||
_viewModel = StateObject(wrappedValue: PrivateChatViewModel(chatId: chat.chatId))
|
_viewModel = StateObject(wrappedValue: PrivateChatViewModel(chatId: chat.chatId, currentUserId: currentUserId))
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -37,6 +39,9 @@ struct PrivateChatView: View {
|
|||||||
hasPositionedToBottom = false
|
hasPositionedToBottom = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.safeAreaInset(edge: .bottom) {
|
||||||
|
composer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -185,6 +190,48 @@ struct PrivateChatView: View {
|
|||||||
.padding(.top, 8)
|
.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 = {
|
private static let timeFormatter: DateFormatter = {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateStyle = .none
|
formatter.dateStyle = .none
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user