aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS/MessageView.swift
blob: b0c8baa9ad19ced579ef61adb6d8b192814178a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import SwiftUI

struct MessageView: View {
  @Environment(HotlineState.self) private var model: HotlineState
  @Environment(\.colorScheme) private var colorScheme
  
  @State private var input: String = ""
  @State private var scrollPos: Int?
  @State private var contentHeight: CGFloat = 0
  @Namespace private var bottomID
  @FocusState private var focusedField: FocusedField?
  
  var userID: UInt16
    
  var body: some View {
    ScrollViewReader { reader in
      VStack(alignment: .leading, spacing: 0) {
        
        // MARK: Scroll View
        GeometryReader { gm in
          ScrollView(.vertical) {
            LazyVStack(alignment: .leading) {
              ForEach(model.instantMessages[userID] ?? [InstantMessage]()) { msg in
                HStack(alignment: .firstTextBaseline) {
                  if msg.direction == .outgoing {
                    Spacer()
                  }
                  
                  Text(LocalizedStringKey(msg.text))
                    .lineSpacing(4)
                    .multilineTextAlignment(.leading)
                    .textSelection(.enabled)
                    .tint(msg.direction == .outgoing ? Color("Outgoing Message Link") : Color("Link Color"))
                    .foregroundStyle(msg.direction == .outgoing ? Color("Outgoing Message Text") : Color("Incoming Message Text"))
                    .padding(EdgeInsets(top: 10, leading: 14, bottom: 10, trailing: 14))
                    .background(msg.direction == .outgoing ? Color("Outgoing Message Background") : Color("Incoming Message Background"))
                    .clipShape(RoundedRectangle(cornerRadius: 16))
                  
                  if msg.direction == .incoming {
                    Spacer()
                  }
                }
                .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
              }
            }
            .padding()
            
            VStack(spacing: 0) {}.id(bottomID)
          }
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .defaultScrollAnchor(.bottom)
          .onChange(of: model.instantMessages[userID]?.count) {
            reader.scrollTo(bottomID, anchor: .bottom)
            model.markInstantMessagesAsRead(userID: userID)
          }
          .onAppear {
            reader.scrollTo(bottomID, anchor: .bottom)
            model.markInstantMessagesAsRead(userID: userID)
          }
          .onChange(of: gm.size) {
            reader.scrollTo(bottomID, anchor: .bottom)
          }
        }
        
        // MARK: Input Divider
        Divider()
        
        // MARK: Input Bar
        HStack(alignment: .lastTextBaseline, spacing: 0) {
          let user = model.users.first(where: { $0.id == userID })
          TextField("Message \(user?.name ?? "")", text: $input, axis: .vertical)
            .focused($focusedField, equals: .chatInput)
            .textFieldStyle(.plain)
            .lineLimit(1...5)
            .multilineTextAlignment(.leading)
            .onSubmit {
              if !self.input.isEmpty {
                let message = self.input
                let uid = self.userID
                Task {
                  try? await model.sendInstantMessage(message, userID: uid)
                }
              }
              self.input = ""
            }
            .frame(maxWidth: .infinity)
            .padding()
        }
        .frame(maxWidth: .infinity, minHeight: 28)
        .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
        .overlay(alignment: .leadingFirstTextBaseline) {
          Image(systemName: "chevron.right").opacity(0.4).offset(x: 16)
        }
        .onContinuousHover { phase in
          switch phase {
          case .active(_):
            NSCursor.iBeam.set()
          case .ended:
            NSCursor.arrow.set()
            break
          }
        }
        .onTapGesture {
          focusedField = .chatInput
        }
      }
      .background(Color(nsColor: .textBackgroundColor))
    }
  }
}

#Preview {
  ChatView()
    .environment(HotlineState())
}