Compare commits
5 Commits
ec70d1f59d
...
452ccfacd6
| Author | SHA1 | Date | |
|---|---|---|---|
| 452ccfacd6 | |||
| 28d2525e4a | |||
| 6b7bc45fed | |||
| cef8a13237 | |||
| 59d2f8c161 |
@ -598,6 +598,9 @@
|
||||
},
|
||||
"Добро пожаловать в Yobble!" : {
|
||||
|
||||
},
|
||||
"Дождитесь отправки предыдущего сообщения." : {
|
||||
|
||||
},
|
||||
"Дополнительные действия." : {
|
||||
"comment" : "Message profile more action description"
|
||||
@ -1910,6 +1913,9 @@
|
||||
},
|
||||
"Ошибка в данных. Проверьте введённую информацию." : {
|
||||
|
||||
},
|
||||
"Ошибка отправки" : {
|
||||
|
||||
},
|
||||
"Ошибка при деавторизации." : {
|
||||
"localizations" : {
|
||||
|
||||
@ -6,6 +6,7 @@ final class PrivateChatViewModel: ObservableObject {
|
||||
@Published private(set) var isInitialLoading: Bool = false
|
||||
@Published private(set) var isLoadingMore: Bool = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var sendingErrorMessage: String?
|
||||
@Published private(set) var isSending: Bool = false
|
||||
@Published private(set) var hasMore: Bool = true
|
||||
|
||||
@ -74,11 +75,12 @@ final class PrivateChatViewModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
guard trimmed.count <= maxMessageLength else {
|
||||
errorMessage = NSLocalizedString("Сообщение слишком длинное.", comment: "")
|
||||
sendingErrorMessage = NSLocalizedString("Сообщение слишком длинное.", comment: "")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard !isSending else {
|
||||
sendingErrorMessage = NSLocalizedString("Дождитесь отправки предыдущего сообщения.", comment: "")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
@ -88,6 +90,7 @@ final class PrivateChatViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
isSending = true
|
||||
sendingErrorMessage = nil
|
||||
|
||||
chatService.sendPrivateMessage(chatId: chatId, content: trimmed) { [weak self] result in
|
||||
guard let self else { return }
|
||||
@ -113,7 +116,7 @@ final class PrivateChatViewModel: ObservableObject {
|
||||
self.errorMessage = nil
|
||||
completion(true)
|
||||
case .failure(let error):
|
||||
self.errorMessage = self.message(for: error)
|
||||
self.sendingErrorMessage = self.message(for: error)
|
||||
completion(false)
|
||||
}
|
||||
|
||||
@ -213,4 +216,4 @@ final class PrivateChatViewModel: ObservableObject {
|
||||
|
||||
return lhs.messageId < rhs.messageId
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,7 @@ struct PrivateChatView: View {
|
||||
@State private var legacyComposerHeight: CGFloat = 40
|
||||
@State private var isProfilePresented: Bool = false
|
||||
@FocusState private var isComposerFocused: Bool
|
||||
@EnvironmentObject private var themeManager: ThemeManager
|
||||
@EnvironmentObject private var messageCenter: IncomingMessageCenter
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@ -65,6 +66,14 @@ struct PrivateChatView: View {
|
||||
}
|
||||
.hidden()
|
||||
}
|
||||
.alert("Ошибка отправки", isPresented: Binding(
|
||||
get: { viewModel.sendingErrorMessage != nil },
|
||||
set: { if !$0 { viewModel.sendingErrorMessage = nil } }
|
||||
), actions: {
|
||||
Button("OK") { }
|
||||
}, message: {
|
||||
Text(viewModel.sendingErrorMessage ?? "Не удалось отправить сообщение.")
|
||||
})
|
||||
.navigationTitle(toolbarTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
// .navigationBarBackButtonHidden(true)
|
||||
@ -244,104 +253,213 @@ struct PrivateChatView: View {
|
||||
|
||||
let topPadding: CGFloat = decoratedMessage.showHorns ? 5 : -12
|
||||
let verticalPadding: CGFloat = hasDecorations ? 0 : 0
|
||||
|
||||
return HStack(alignment: .bottom, spacing: 8) {
|
||||
if isCurrentUser { Spacer(minLength: 32) }
|
||||
|
||||
messageBubble(
|
||||
for: message,
|
||||
decorations: decoratedMessage,
|
||||
isCurrentUser: isCurrentUser
|
||||
)
|
||||
|
||||
if !isCurrentUser { Spacer(minLength: 32) }
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, topPadding)
|
||||
.padding(.vertical, verticalPadding)
|
||||
.contextMenu {
|
||||
if isCurrentUser {
|
||||
if message.isViewed == true {
|
||||
Text("Прочитано") // Placeholder for read status
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
return HStack(alignment: .bottom, spacing: 8) {
|
||||
if isCurrentUser { Spacer(minLength: 32) }
|
||||
|
||||
messageBubble(
|
||||
for: message,
|
||||
decorations: decoratedMessage,
|
||||
isCurrentUser: isCurrentUser
|
||||
)
|
||||
|
||||
if !isCurrentUser { Spacer(minLength: 32) }
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, topPadding)
|
||||
.padding(.vertical, verticalPadding)
|
||||
.contextMenu(menuItems: {
|
||||
if isCurrentUser {
|
||||
if message.isViewed == true {
|
||||
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")
|
||||
}
|
||||
}
|
||||
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: {
|
||||
messageBubble(
|
||||
for: message,
|
||||
decorations: decoratedMessage,
|
||||
isCurrentUser: isCurrentUser
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return HStack(alignment: .bottom, spacing: 8) {
|
||||
if isCurrentUser { Spacer(minLength: 32) }
|
||||
|
||||
messageBubble(
|
||||
for: message,
|
||||
decorations: decoratedMessage,
|
||||
isCurrentUser: isCurrentUser
|
||||
)
|
||||
|
||||
if !isCurrentUser { Spacer(minLength: 32) }
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, topPadding)
|
||||
.padding(.vertical, verticalPadding)
|
||||
.contextMenu {
|
||||
if isCurrentUser {
|
||||
if message.isViewed == true {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -538,7 +656,7 @@ struct PrivateChatView: View {
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 8)
|
||||
.background(.ultraThinMaterial)
|
||||
.modifier(ComposerBackgroundModifier(theme: themeManager.theme))
|
||||
}
|
||||
|
||||
private func scrollToBottomButton(proxy: ScrollViewProxy) -> some View {
|
||||
@ -613,10 +731,10 @@ struct PrivateChatView: View {
|
||||
let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else { return }
|
||||
|
||||
draftText = ""
|
||||
scrollToBottomTrigger = .init()
|
||||
viewModel.sendMessage(text: text) { success in
|
||||
if success {
|
||||
draftText = ""
|
||||
hasPositionedToBottom = true
|
||||
}
|
||||
}
|
||||
@ -912,6 +1030,28 @@ 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.
|
||||
private struct DecoratedMessage: Identifiable {
|
||||
let message: MessageItem
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user