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
|
import SwiftUI
struct MessageView: View {
@Environment(Hotline.self) private var model: Hotline
@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 {
model.sendInstantMessage(self.input, userID: self.userID)
}
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(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()))
}
|