Compare commits

..

No commits in common. "452ccfacd6bdf6badce21ebee565c906faf37987" and "ec70d1f59d0ae39ae3805874f3585e357de6df5b" have entirely different histories.

3 changed files with 102 additions and 251 deletions

View File

@ -598,9 +598,6 @@
}, },
"Добро пожаловать в Yobble!" : { "Добро пожаловать в Yobble!" : {
},
"Дождитесь отправки предыдущего сообщения." : {
}, },
"Дополнительные действия." : { "Дополнительные действия." : {
"comment" : "Message profile more action description" "comment" : "Message profile more action description"
@ -1913,9 +1910,6 @@
}, },
"Ошибка в данных. Проверьте введённую информацию." : { "Ошибка в данных. Проверьте введённую информацию." : {
},
"Ошибка отправки" : {
}, },
"Ошибка при деавторизации." : { "Ошибка при деавторизации." : {
"localizations" : { "localizations" : {

View File

@ -6,7 +6,6 @@ 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 var sendingErrorMessage: String?
@Published private(set) var isSending: Bool = false @Published private(set) var isSending: Bool = false
@Published private(set) var hasMore: Bool = true @Published private(set) var hasMore: Bool = true
@ -75,12 +74,11 @@ final class PrivateChatViewModel: ObservableObject {
return return
} }
guard trimmed.count <= maxMessageLength else { guard trimmed.count <= maxMessageLength else {
sendingErrorMessage = NSLocalizedString("Сообщение слишком длинное.", comment: "") errorMessage = NSLocalizedString("Сообщение слишком длинное.", comment: "")
completion(false) completion(false)
return return
} }
guard !isSending else { guard !isSending else {
sendingErrorMessage = NSLocalizedString("Дождитесь отправки предыдущего сообщения.", comment: "")
completion(false) completion(false)
return return
} }
@ -90,7 +88,6 @@ final class PrivateChatViewModel: ObservableObject {
} }
isSending = true isSending = true
sendingErrorMessage = nil
chatService.sendPrivateMessage(chatId: chatId, content: trimmed) { [weak self] result in chatService.sendPrivateMessage(chatId: chatId, content: trimmed) { [weak self] result in
guard let self else { return } guard let self else { return }
@ -116,7 +113,7 @@ final class PrivateChatViewModel: ObservableObject {
self.errorMessage = nil self.errorMessage = nil
completion(true) completion(true)
case .failure(let error): case .failure(let error):
self.sendingErrorMessage = self.message(for: error) self.errorMessage = self.message(for: error)
completion(false) completion(false)
} }
@ -216,4 +213,4 @@ final class PrivateChatViewModel: ObservableObject {
return lhs.messageId < rhs.messageId return lhs.messageId < rhs.messageId
} }
} }

View File

@ -22,7 +22,6 @@ struct PrivateChatView: View {
@State private var legacyComposerHeight: CGFloat = 40 @State private var legacyComposerHeight: CGFloat = 40
@State private var isProfilePresented: Bool = false @State private var isProfilePresented: Bool = false
@FocusState private var isComposerFocused: Bool @FocusState private var isComposerFocused: Bool
@EnvironmentObject private var themeManager: ThemeManager
@EnvironmentObject private var messageCenter: IncomingMessageCenter @EnvironmentObject private var messageCenter: IncomingMessageCenter
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ -66,14 +65,6 @@ struct PrivateChatView: View {
} }
.hidden() .hidden()
} }
.alert("Ошибка отправки", isPresented: Binding(
get: { viewModel.sendingErrorMessage != nil },
set: { if !$0 { viewModel.sendingErrorMessage = nil } }
), actions: {
Button("OK") { }
}, message: {
Text(viewModel.sendingErrorMessage ?? "Не удалось отправить сообщение.")
})
.navigationTitle(toolbarTitle) .navigationTitle(toolbarTitle)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
// .navigationBarBackButtonHidden(true) // .navigationBarBackButtonHidden(true)
@ -253,213 +244,104 @@ struct PrivateChatView: View {
let topPadding: CGFloat = decoratedMessage.showHorns ? 5 : -12 let topPadding: CGFloat = decoratedMessage.showHorns ? 5 : -12
let verticalPadding: CGFloat = hasDecorations ? 0 : 0 let verticalPadding: CGFloat = hasDecorations ? 0 : 0
if #available(iOS 16.0, *) { return HStack(alignment: .bottom, spacing: 8) {
return HStack(alignment: .bottom, spacing: 8) { if isCurrentUser { Spacer(minLength: 32) }
if isCurrentUser { Spacer(minLength: 32) }
messageBubble(
messageBubble( for: message,
for: message, decorations: decoratedMessage,
decorations: decoratedMessage, isCurrentUser: isCurrentUser
isCurrentUser: isCurrentUser )
)
if !isCurrentUser { Spacer(minLength: 32) }
if !isCurrentUser { Spacer(minLength: 32) } }
} .padding(.horizontal, 8)
.padding(.horizontal, 8) .padding(.top, topPadding)
.padding(.top, topPadding) .padding(.vertical, verticalPadding)
.padding(.vertical, verticalPadding) .contextMenu {
.contextMenu(menuItems: { if isCurrentUser {
if isCurrentUser { if message.isViewed == true {
if message.isViewed == true { Text("Прочитано") // Placeholder for read status
Text("Прочитано") // Placeholder for read status
}
Button(action: {
// Reply action
}) {
Text("Ответить")
Image(systemName: "arrowshape.turn.up.left")
}
Button(action: {
// Copy action
}) {
Text("Копировать")
Image(systemName: "doc.on.doc")
}
Button(action: {
// Edit action
}) {
Text("Изменить")
Image(systemName: "pencil")
}
Button(action: {
// Pin action
}) {
Text("Закрепить")
Image(systemName: "pin")
}
Button(action: {
// Forward action
}) {
Text("Переслать")
Image(systemName: "arrowshape.turn.up.right")
}
Button(role: .destructive, action: {
// Delete action
}) {
Text("Удалить")
Image(systemName: "trash")
}
Button(action: {
// Select action
}) {
Text("Выбрать")
Image(systemName: "checkmark.circle")
}
} else {
Button(action: {
// Reply action
}) {
Text("Ответить")
Image(systemName: "arrowshape.turn.up.left")
}
Button(action: {
// Copy action
}) {
Text("Копировать")
Image(systemName: "doc.on.doc")
}
Button(action: {
// Pin action
}) {
Text("Закрепить")
Image(systemName: "pin")
}
Button(action: {
// Forward action
}) {
Text("Переслать")
Image(systemName: "arrowshape.turn.up.right")
}
Button(role: .destructive, action: {
// Delete action
}) {
Text("Удалить")
Image(systemName: "trash")
}
Button(action: {
// Select action
}) {
Text("Выбрать")
Image(systemName: "checkmark.circle")
}
} }
}, preview: { Button(action: {
messageBubble( // Reply action
for: message, }) {
decorations: decoratedMessage, Text("Ответить")
isCurrentUser: isCurrentUser Image(systemName: "arrowshape.turn.up.left")
) }
}) Button(action: {
} else { // Copy action
return HStack(alignment: .bottom, spacing: 8) { }) {
if isCurrentUser { Spacer(minLength: 32) } Text("Копировать")
Image(systemName: "doc.on.doc")
messageBubble( }
for: message, Button(action: {
decorations: decoratedMessage, // Edit action
isCurrentUser: isCurrentUser }) {
) Text("Изменить")
Image(systemName: "pencil")
if !isCurrentUser { Spacer(minLength: 32) } }
} Button(action: {
.padding(.horizontal, 8) // Pin action
.padding(.top, topPadding) }) {
.padding(.vertical, verticalPadding) Text("Закрепить")
.contextMenu { Image(systemName: "pin")
if isCurrentUser { }
if message.isViewed == true { Button(action: {
Text("Прочитано") // Placeholder for read status // Forward action
} }) {
Button(action: { Text("Переслать")
// Reply action Image(systemName: "arrowshape.turn.up.right")
}) { }
Text("Ответить") Button(role: .destructive, action: {
Image(systemName: "arrowshape.turn.up.left") // Delete action
} }) {
Button(action: { Text("Удалить")
// Copy action Image(systemName: "trash")
}) { }
Text("Копировать") Button(action: {
Image(systemName: "doc.on.doc") // Select action
} }) {
Button(action: { Text("Выбрать")
// Edit action Image(systemName: "checkmark.circle")
}) { }
Text("Изменить") } else {
Image(systemName: "pencil") Button(action: {
} // Reply action
Button(action: { }) {
// Pin action Text("Ответить")
}) { Image(systemName: "arrowshape.turn.up.left")
Text("Закрепить") }
Image(systemName: "pin") Button(action: {
} // Copy action
Button(action: { }) {
// Forward action Text("Копировать")
}) { Image(systemName: "doc.on.doc")
Text("Переслать") }
Image(systemName: "arrowshape.turn.up.right") Button(action: {
} // Pin action
Button(role: .destructive, action: { }) {
// Delete action Text("Закрепить")
}) { Image(systemName: "pin")
Text("Удалить") }
Image(systemName: "trash") Button(action: {
} // Forward action
Button(action: { }) {
// Select action Text("Переслать")
}) { Image(systemName: "arrowshape.turn.up.right")
Text("Выбрать") }
Image(systemName: "checkmark.circle") Button(role: .destructive, action: {
} // Delete action
} else { }) {
Button(action: { Text("Удалить")
// Reply action Image(systemName: "trash")
}) { }
Text("Ответить") Button(action: {
Image(systemName: "arrowshape.turn.up.left") // Select action
} }) {
Button(action: { Text("Выбрать")
// Copy action Image(systemName: "checkmark.circle")
}) {
Text("Копировать")
Image(systemName: "doc.on.doc")
}
Button(action: {
// Pin action
}) {
Text("Закрепить")
Image(systemName: "pin")
}
Button(action: {
// Forward action
}) {
Text("Переслать")
Image(systemName: "arrowshape.turn.up.right")
}
Button(role: .destructive, action: {
// Delete action
}) {
Text("Удалить")
Image(systemName: "trash")
}
Button(action: {
// Select action
}) {
Text("Выбрать")
Image(systemName: "checkmark.circle")
}
} }
} }
} }
@ -656,7 +538,7 @@ struct PrivateChatView: View {
.padding(.horizontal, 6) .padding(.horizontal, 6)
.padding(.top, 10) .padding(.top, 10)
.padding(.bottom, 8) .padding(.bottom, 8)
.modifier(ComposerBackgroundModifier(theme: themeManager.theme)) .background(.ultraThinMaterial)
} }
private func scrollToBottomButton(proxy: ScrollViewProxy) -> some View { private func scrollToBottomButton(proxy: ScrollViewProxy) -> some View {
@ -731,10 +613,10 @@ struct PrivateChatView: View {
let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines) let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return } guard !text.isEmpty else { return }
draftText = ""
scrollToBottomTrigger = .init() scrollToBottomTrigger = .init()
viewModel.sendMessage(text: text) { success in viewModel.sendMessage(text: text) { success in
if success { if success {
draftText = ""
hasPositionedToBottom = true hasPositionedToBottom = true
} }
} }
@ -1030,28 +912,6 @@ private var headerPlaceholderAvatar: some View {
} }
private struct ComposerBackgroundModifier: ViewModifier {
let theme: Theme
@ViewBuilder
func body(content: Content) -> some View {
if theme == .oledDark {
content
.background(Color.black.opacity(0.85)) // глубокий чёрный
// .overlay(
// Color.black.opacity(0.25) // лёгкое затемнение сверху
// .allowsHitTesting(false)
// )
// content.background(.ultraThinMaterial)
// content.background(Color.black)
// content.background(Color(white: 0.15))
} else {
content.background(.ultraThinMaterial)
}
}
}
/// Helper model that stores a message alongside horn/leg flags for grouping sequences. /// Helper model that stores a message alongside horn/leg flags for grouping sequences.
private struct DecoratedMessage: Identifiable { private struct DecoratedMessage: Identifiable {
let message: MessageItem let message: MessageItem