1 /*
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  * All rights reserved.
4  *
5  * This source code is licensed under the BSD-style license found in the
6  * LICENSE file in the root directory of this source tree.
7  */
8 
9 import SwiftUI
10 
11 struct MessageListView: View {
12   @Binding var messages: [Message]
13   @State private var showScrollToBottomButton = false
14   @State private var userHasScrolled = false
15   @State private var keyboardHeight: CGFloat = 0
16 
17   var body: some View {
18     ScrollViewReader { value in
19       ScrollView {
20         VStack {
21           ForEach(messages) { message in
22             MessageView(message: message)
23               .padding([.leading, .trailing], 20)
24           }
25           GeometryReader { geometry -> Color in
26             DispatchQueue.main.async {
27               let maxY = geometry.frame(in: .global).maxY
28               let screenHeight = UIScreen.main.bounds.height - keyboardHeight
29               let isBeyondBounds = maxY > screenHeight - 50
30               if showScrollToBottomButton != isBeyondBounds {
31                 showScrollToBottomButton = isBeyondBounds
32                 userHasScrolled = isBeyondBounds
33               }
34             }
35             return Color.clear
36           }
37           .frame(height: 0)
38         }
39       }
40       .onChange(of: messages) {
41         if !userHasScrolled, let lastMessageId = messages.last?.id {
42           withAnimation {
43             value.scrollTo(lastMessageId, anchor: .bottom)
44           }
45         }
46       }
47       .overlay(
48         Group {
49           if showScrollToBottomButton {
50             Button(action: {
51               withAnimation {
52                 if let lastMessageId = messages.last?.id {
53                   value.scrollTo(lastMessageId, anchor: .bottom)
54                 }
55                 userHasScrolled = false
56               }
57             }) {
58               ZStack {
59                 Circle()
60                   .fill(Color(UIColor.secondarySystemBackground).opacity(0.9))
61                   .frame(height: 28)
62                 Image(systemName: "arrow.down.circle")
63                   .resizable()
64                   .aspectRatio(contentMode: .fit)
65                   .frame(height: 28)
66               }
67             }
68             .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2)))
69           }
70         },
71         alignment: .bottom
72       )
73     }
74     .onAppear {
75       NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in
76         let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero
77         keyboardHeight = keyboardFrame.height - 40
78       }
79       NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
80         keyboardHeight = 0
81       }
82     }
83     .onDisappear {
84       NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
85       NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
86     }
87   }
88 }
89