diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index ecc121e..1a4cc10 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -270,6 +270,9 @@ }, "Вложение" : { + }, + "Вложения" : { + }, "Войти" : { "localizations" : { @@ -1526,6 +1529,9 @@ }, "Поиск отменён." : { "comment" : "Search cancelled" + }, + "Поиск стикеров" : { + }, "Пока что у вас нет чатов" : { "localizations" : { @@ -2095,6 +2101,9 @@ } } } + }, + "Стикеры" : { + }, "Тема: %@" : { "comment" : "feedback: success category", diff --git a/yobble/Views/Chat/PrivateChatView.swift b/yobble/Views/Chat/PrivateChatView.swift index 38980d7..958bc2b 100644 --- a/yobble/Views/Chat/PrivateChatView.swift +++ b/yobble/Views/Chat/PrivateChatView.swift @@ -7,6 +7,8 @@ struct PrivateChatView: View { @StateObject private var viewModel: PrivateChatViewModel @State private var hasPositionedToBottom: Bool = false @State private var draftText: String = "" + @State private var inputTab: ComposerTab = .chat + @State private var isVideoPreferred: Bool = false @FocusState private var isComposerFocused: Bool @EnvironmentObject private var messageCenter: IncomingMessageCenter @@ -206,28 +208,54 @@ struct PrivateChatView: View { } private var composer: some View { - VStack(spacing: 0) { + VStack(spacing: 10) { 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)) + HStack(spacing: 12) { + composerToolbarButton(systemName: "paperclip", label: NSLocalizedString("Вложения", comment: "")) { + // TODO: hook to attachments action + } + + composerModeButton(.chat) + composerModeButton(.stickers) + + Spacer(minLength: 0) + } + + HStack(alignment: .bottom, spacing: 12) { + HStack(alignment: .center, spacing: 8) { + Image(systemName: inputTab.iconName) + .foregroundColor(.secondary) + TextField(inputTab.placeholder, text: $draftText, axis: .vertical) + .lineLimit(1...4) + .focused($isComposerFocused) + .submitLabel(.send) + .disabled(viewModel.isSending || currentUserId == nil) + .onSubmit { sendCurrentMessage() } + } + .padding(.vertical, 10) + .padding(.horizontal, 12) + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + + if !isSendAvailable { + Button(action: { isVideoPreferred.toggle() }) { + Image(systemName: isVideoPreferred ? "video.fill" : "mic.fill") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.accentColor) + .frame(width: 40, height: 40) + .background(Color.accentColor.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + } else { + sendButton } - .disabled(isSendDisabled) - .buttonStyle(.plain) } - .padding(.vertical, 10) - .padding(.horizontal, 16) - .background(.clear) } + .padding(.horizontal, 16) + .padding(.top, 10) + .padding(.bottom, 8) .background(.ultraThinMaterial) } @@ -235,6 +263,61 @@ struct PrivateChatView: View { draftText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending || currentUserId == nil } + private var isSendAvailable: Bool { + !draftText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && currentUserId != nil + } + + private var sendButton: some View { + Button(action: sendCurrentMessage) { + Image(systemName: viewModel.isSending ? "hourglass" : "paperplane.fill") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(Color.accentColor) + .clipShape(Circle()) + } + .disabled(isSendDisabled) + .buttonStyle(.plain) + } + + private func composerToolbarButton(systemName: String, label: String, action: @escaping () -> Void) -> some View { + Button(action: action) { + VStack(spacing: 4) { + Image(systemName: systemName) + .font(.system(size: 16, weight: .medium)) + Text(label) + .font(.caption2) + } + .foregroundColor(.primary) + .frame(width: 64, height: 44) + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + } + .buttonStyle(.plain) + } + + private func composerModeButton(_ tab: ComposerTab) -> some View { + Button(action: { inputTab = tab }) { + Text(tab.title) + .font(.caption) + .fontWeight(inputTab == tab ? .semibold : .regular) + .padding(.vertical, 8) + .padding(.horizontal, 14) + .background( + Group { + if inputTab == tab { + Color.accentColor.opacity(0.15) + } else { + Color(.secondarySystemBackground) + } + } + ) + .foregroundColor(inputTab == tab ? .accentColor : .primary) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + private func sendCurrentMessage() { let text = draftText.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return } @@ -247,6 +330,32 @@ struct PrivateChatView: View { } } + private enum ComposerTab: String { + case chat + case stickers + + var title: String { + switch self { + case .chat: return NSLocalizedString("Чат", comment: "") + case .stickers: return NSLocalizedString("Стикеры", comment: "") + } + } + + var iconName: String { + switch self { + case .chat: return "text.bubble" + case .stickers: return "face.smiling" + } + } + + var placeholder: String { + switch self { + case .chat: return NSLocalizedString("Сообщение", comment: "") + case .stickers: return NSLocalizedString("Поиск стикеров", comment: "") + } + } + } + private static let timeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .none