IceCubesApp/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift

166 lines
4.9 KiB
Swift
Raw Normal View History

2023-01-22 15:55:03 +00:00
import DesignSystem
import Env
import Models
import Network
2023-01-22 15:55:03 +00:00
import NukeUI
import SwiftUI
2023-01-22 15:55:03 +00:00
2023-09-18 19:03:52 +00:00
@MainActor
2023-01-22 15:55:03 +00:00
public struct ConversationDetailView: View {
private enum Constants {
static let bottomAnchor = "bottom"
}
@Environment(QuickLook.self) private var quickLook
@Environment(RouterPath.self) private var routerPath
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Client.self) private var client
2023-09-18 19:03:52 +00:00
@Environment(Theme.self) private var theme
@Environment(StreamWatcher.self) private var watcher
@State private var viewModel: ConversationDetailViewModel
2023-01-22 15:55:03 +00:00
@FocusState private var isMessageFieldFocused: Bool
2023-01-22 15:55:03 +00:00
@State private var scrollProxy: ScrollViewProxy?
@State private var didAppear: Bool = false
2023-01-22 15:55:03 +00:00
public init(conversation: Conversation) {
_viewModel = .init(initialValue: .init(conversation: conversation))
2023-01-22 15:55:03 +00:00
}
2023-01-22 15:55:03 +00:00
public var body: some View {
ScrollViewReader { proxy in
2023-02-21 18:38:35 +00:00
ScrollView {
LazyVStack {
if viewModel.isLoadingMessages {
loadingView
2023-01-22 15:55:03 +00:00
}
2023-02-21 18:38:35 +00:00
ForEach(viewModel.messages) { message in
ConversationMessageView(message: message,
conversation: viewModel.conversation)
.padding(.vertical, 4)
.id(message.id)
}
bottomAnchorView
2023-01-22 15:55:03 +00:00
}
2023-02-21 18:38:35 +00:00
.padding(.horizontal, .layoutPadding)
}
#if !os(visionOS)
2023-02-21 18:38:35 +00:00
.scrollDismissesKeyboard(.interactively)
#endif
2023-02-21 18:38:35 +00:00
.safeAreaInset(edge: .bottom) {
2023-01-22 15:55:03 +00:00
inputTextView
}
.onAppear {
scrollProxy = proxy
viewModel.client = client
isMessageFieldFocused = true
if !didAppear {
didAppear = true
Task {
await viewModel.fetchMessages()
DispatchQueue.main.async {
withAnimation {
proxy.scrollTo(Constants.bottomAnchor, anchor: .bottom)
}
}
}
}
}
}
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
2024-02-14 11:48:14 +00:00
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
2024-02-14 11:48:14 +00:00
.toolbar {
ToolbarItem(placement: .principal) {
if viewModel.conversation.accounts.count == 1,
let account = viewModel.conversation.accounts.first
{
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
.font(.scaledHeadline)
.foregroundColor(theme.labelColor)
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
} else {
Text("Direct message with \(viewModel.conversation.accounts.count) people")
.font(.scaledHeadline)
}
2023-01-22 15:55:03 +00:00
}
}
2024-02-14 11:48:14 +00:00
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent {
viewModel.handleEvent(event: latestEvent)
DispatchQueue.main.async {
withAnimation {
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
}
2023-01-22 15:55:03 +00:00
}
}
}
}
2023-01-22 15:55:03 +00:00
private var loadingView: some View {
ForEach(Status.placeholders()) { message in
ConversationMessageView(message: message, conversation: viewModel.conversation)
.redacted(reason: .placeholder)
2023-09-18 16:55:11 +00:00
.allowsHitTesting(false)
.padding(.vertical, 4)
2023-01-22 15:55:03 +00:00
}
}
2023-01-22 15:55:03 +00:00
private var bottomAnchorView: some View {
Rectangle()
.fill(Color.clear)
.frame(height: 40)
.id(Constants.bottomAnchor)
Timeline & Timeline detail accessibility uplift (#1323) * Improve accessibility of StatusPollView Previously, this view did not provide the proper context to indicate that it represented a poll. Now, we’ve added - A container that will stay “Active poll” or “Poll results” when the cursor first hits one of the options; - A prefix to say “Option X of Y” before each option; - A Selected trait on the selected option(s), if present - Consolidating and adding an `.updatesFrequently` trait to the footer view with the countdown. * Add poll description in StatusRowView combinedAccessibilityLabel This largely duplicates the logic in `StatusPollView`. * Improve accessibility of media attachments Previously, the media attachments without alt text would not show up in the consolidated `StatusRowView`, nor would they be meaningfully explained on the status detail screen. Now, they are presented with their attachment type. * Change accessibilityRepresentation of AppAcountsSelectorView * Change Notifications tab title view accessibility representation to Menu Previously it would present as a button * Hide layout `Rectangle`s from accessibility * Consolidate `StatusRowDetailView` accessibility representation * Improve readability of Poll accessibility label * Ensure poll options don’t present as interactive when the poll is finished * Improve accessibility of StatusRowCardView Previously, it would present as four separate elements, including an image without a description, all interactive, none with an interactive trait. Now, it presents as a single element with the `.link` trait * Improve accessibility of StatusRowHeaderView Previously, it had no traits and no actions except inherited ones. Now it presents as a button, triggering its primary action. It also has custom actions corresponding to its context menu * Avoid applying the StatusRowView custom actions to every view when contained * Provide context for the application name * Add pauses to StatusRowView combinedAccessibilityLabel * Hide `TimelineView.scrollToTopView` from accessibility * Set appropriate font style on Notification header After the change the Text needed a `.headline` style to match the prior appearance. * Fix bug in accessibilityRepresentation of TimelineView nav bar title Previously, it would not display the proper label for .remoteLocal filter options. * Ensure that pop-up button nav bar titles are interactive * Ensure TextView responds to Environment.sizeCategory This resolves #1309 * Fix button --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-28 16:48:58 +00:00
.accessibilityHidden(true)
2023-01-22 15:55:03 +00:00
}
2023-01-22 15:55:03 +00:00
private var inputTextView: some View {
2023-01-22 16:17:33 +00:00
VStack {
HStack(alignment: .bottom, spacing: 8) {
if viewModel.conversation.lastStatus != nil {
Button {
routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.conversation.lastStatus!)
} label: {
Image(systemName: "plus")
}
.padding(.bottom, 7)
2023-01-22 15:55:03 +00:00
}
TextField("conversations.new.message.placeholder", text: $viewModel.newMessageText, axis: .vertical)
2023-01-22 15:55:03 +00:00
.focused($isMessageFieldFocused)
2023-01-22 16:17:33 +00:00
.keyboardType(.default)
2023-01-26 17:27:53 +00:00
.backgroundStyle(.thickMaterial)
.padding(6)
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(.gray, lineWidth: 1)
2023-01-27 19:36:40 +00:00
)
.font(.scaledBody)
2023-01-22 15:55:03 +00:00
if !viewModel.newMessageText.isEmpty {
Button {
Task {
2023-09-18 07:18:48 +00:00
guard !viewModel.isSendingMessage else { return }
2023-01-22 15:55:03 +00:00
await viewModel.postMessage()
}
} label: {
if viewModel.isSendingMessage {
ProgressView()
} else {
Image(systemName: "paperplane")
}
}
.keyboardShortcut(.return, modifiers: .command)
2023-01-26 17:27:53 +00:00
.padding(.bottom, 6)
2023-01-22 15:55:03 +00:00
}
}
.padding(8)
}
.background(.thinMaterial)
}
}