diff --git a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift index 59c9c29..fbd88eb 100644 --- a/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift +++ b/apps/ios/TeleTuiIOS/Sources/TeleTuiIOSCore/Views.swift @@ -341,63 +341,75 @@ public struct ChatDetailView: View { public var body: some View { VStack(spacing: 0) { - List { - if !viewModel.searchText.isEmpty { - Section("Search") { - if viewModel.searchResults.isEmpty { - Text("No results") - .foregroundStyle(.secondary) - } else { - ForEach(viewModel.searchResults) { message in - MessageRow(message: message) - .listRowSeparator(.hidden) + ScrollViewReader { scrollProxy in + VStack(spacing: 0) { + if !viewModel.pinnedMessages.isEmpty { + PinnedMessagesBar(messages: viewModel.pinnedMessages) { message in + withAnimation { + scrollProxy.scrollTo(message.id, anchor: .center) } } } - } - Section { - ForEach(viewModel.messages) { message in - MessageRow(message: message) - .contextMenu { - Button { - viewModel.beginReply(to: message) - } label: { - Label("Reply", systemImage: "arrowshape.turn.up.left") - } - Button { - editingMessage = message - editedText = message.text - } label: { - Label("Edit", systemImage: "pencil") - } - Button { - forwardCandidate = message - forwardChatIdText = "" - } label: { - Label("Forward", systemImage: "arrowshape.turn.up.forward") - } - Button { - Task { await viewModel.react(message: message, reaction: "👍") } - } label: { - Label("React", systemImage: "face.smiling") - } - Button { - Task { - await viewModel.copyPayload(for: message) - if let payload = viewModel.copiedPayload { - await clipboard.write(text: payload) - } + List { + if !viewModel.searchText.isEmpty { + Section("Search") { + if viewModel.searchResults.isEmpty { + Text("No results") + .foregroundStyle(.secondary) + } else { + ForEach(viewModel.searchResults) { message in + MessageRow(message: message) + .listRowSeparator(.hidden) } - } label: { - Label("Copy", systemImage: "doc.on.doc") - } - Button(role: .destructive) { - deleteCandidate = message - } label: { - Label("Delete", systemImage: "trash") } } - .listRowSeparator(.hidden) + } + Section { + ForEach(viewModel.messages) { message in + MessageRow(message: message) + .id(message.id) + .contextMenu { + Button { + viewModel.beginReply(to: message) + } label: { + Label("Reply", systemImage: "arrowshape.turn.up.left") + } + Button { + editingMessage = message + editedText = message.text + } label: { + Label("Edit", systemImage: "pencil") + } + Button { + forwardCandidate = message + forwardChatIdText = "" + } label: { + Label("Forward", systemImage: "arrowshape.turn.up.forward") + } + Button { + Task { await viewModel.react(message: message, reaction: "👍") } + } label: { + Label("React", systemImage: "face.smiling") + } + Button { + Task { + await viewModel.copyPayload(for: message) + if let payload = viewModel.copiedPayload { + await clipboard.write(text: payload) + } + } + } label: { + Label("Copy", systemImage: "doc.on.doc") + } + Button(role: .destructive) { + deleteCandidate = message + } label: { + Label("Delete", systemImage: "trash") + } + } + .listRowSeparator(.hidden) + } + } } } } @@ -497,6 +509,49 @@ public struct ChatDetailView: View { } } +public struct PinnedMessagesBar: View { + public var messages: [Message] + public var select: (Message) -> Void + + public init(messages: [Message], select: @escaping (Message) -> Void) { + self.messages = messages + self.select = select + } + + public var body: some View { + VStack(spacing: 0) { + ForEach(messages.prefix(3)) { message in + Button { + select(message) + } label: { + HStack(spacing: 8) { + Image(systemName: "pin.fill") + .font(.caption) + .foregroundStyle(.blue) + VStack(alignment: .leading, spacing: 2) { + Text(message.senderName) + .font(.caption2) + .foregroundStyle(.secondary) + Text(message.text) + .font(.subheadline) + .lineLimit(1) + } + Spacer() + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + .background(Color.blue.opacity(0.08)) + .overlay(alignment: .bottom) { + Divider() + } + } +} + public struct MessageRow: View { public var message: Message