@@ -9,6 +9,7 @@ struct ChatInputView: View {
99 @Binding var height : CGFloat ?
1010 @AppStorage ( " fontSize " ) var fontSize : Int = 13
1111
12+ @State private var isDraftLoaded : Bool = false
1213 @State private var chatInput : String = " "
1314 @FocusState private var chatFocused : Bool
1415
@@ -33,6 +34,83 @@ struct ChatInputView: View {
3334 timeline. scrollPosition. scrollTo ( edge: . bottom)
3435 }
3536
37+ private func saveDraft( ) async {
38+ guard isDraftLoaded else { return } // avoid saving a draft hasn't yet been restored
39+ if chatInput. isEmpty && replyTo == nil {
40+ Logger . viewCycle. debug ( " clearing draft " )
41+ do {
42+ try await room. clearComposerDraft ( threadRoot: timeline. focusedThreadId)
43+ } catch {
44+ Logger . viewCycle. error ( " failed to clear draft: \( error) " )
45+ }
46+ return
47+ }
48+
49+ let draftType : ComposerDraftType
50+ if let replyTo {
51+ draftType = . reply( eventId: replyTo. eventOrTransactionId. id)
52+ } else {
53+ draftType = . newMessage
54+ }
55+ let draft = ComposerDraft (
56+ plainText: chatInput,
57+ htmlText: nil ,
58+ draftType: draftType,
59+ attachments: [ ]
60+ )
61+ do {
62+ try await room. saveComposerDraft ( draft: draft, threadRoot: timeline. focusedThreadId)
63+ } catch {
64+ Logger . viewCycle. error ( " failed save draft: \( error) " )
65+ }
66+ }
67+
68+ private func loadDraft( ) async {
69+ guard !isDraftLoaded else { return } // don't load a draft more than once
70+ do {
71+ guard let draft = try await room. loadComposerDraft ( threadRoot: timeline. focusedThreadId) else {
72+ // no draft to load
73+ isDraftLoaded = true
74+ return
75+ }
76+ self . chatInput = draft. plainText
77+ switch draft. draftType {
78+ case . reply( eventId: let eventId) :
79+ // we need a timeline to be able to populate the reply; return false so we can try again
80+ guard let innerTimeline = timeline. timeline else {
81+ isDraftLoaded = false
82+ return
83+ }
84+
85+ do {
86+ let item = try await innerTimeline. getEventTimelineItemByEventId ( eventId: eventId)
87+ self . timeline. sendReplyTo = item
88+ } catch {
89+ Logger . viewCycle. error ( " failed to resolve reply target: \( error) " )
90+ }
91+ case . newMessage, . edit:
92+ // nothing to do
93+ isDraftLoaded = true
94+ return
95+ }
96+ } catch {
97+ Logger . viewCycle. error ( " failed to load draft: \( error) " )
98+ }
99+ isDraftLoaded = true // so we don't try again
100+ }
101+
102+ private func chatInputChanged( ) async {
103+ guard isDraftLoaded else { return } // avoid working on a draft that's being restored
104+ if !chatInput. isEmpty {
105+ do {
106+ try await room. typingNotice ( isTyping: !chatInput. isEmpty)
107+ } catch {
108+ Logger . viewCycle. warning ( " Failed to send typing notice: \( error) " )
109+ }
110+ }
111+ await saveDraft ( )
112+ }
113+
36114 var replyEmbeddedDetails : EmbeddedEventDetails ? {
37115 guard let replyTo else { return nil }
38116
@@ -54,13 +132,13 @@ struct ChatInputView: View {
54132 . scrollContentBackground ( . hidden)
55133 . background ( . clear)
56134 . padding ( 10 )
135+ . disabled ( !isDraftLoaded) // avoid inputs until we've tried to load a draft
57136 }
58137 . font ( . system( size: . init( fontSize) ) )
59138 . background (
60139 GeometryReader { proxy in
61140 Color ( NSColor . textBackgroundColor)
62141 . onChange ( of: proxy. size. height) { _, inputHeight in
63- print ( " Input height: \( inputHeight) " )
64142 self . height = inputHeight
65143 }
66144 }
@@ -76,13 +154,16 @@ struct ChatInputView: View {
76154 . onTapGesture {
77155 chatFocused = true
78156 }
79- . task ( id: !chatInput. isEmpty) {
80- let isTyping = !chatInput. isEmpty
81- do {
82- try await room. typingNotice ( isTyping: isTyping)
83- } catch {
84- Logger . viewCycle. error ( " Failed to set typing notice: \( error) " )
85- }
157+ . task ( id: chatInput) {
158+ await chatInputChanged ( )
159+ }
160+ . task ( id: replyTo? . eventOrTransactionId) {
161+ await saveDraft ( )
162+ }
163+ . task ( id: timeline. timeline != nil ) {
164+ // we need the timeline to be populated before we load a draft
165+ // (in case the draft holds a reply)
166+ await loadDraft ( )
86167 }
87168 . pointerStyle ( . horizontalText)
88169 . padding ( [ . horizontal, . bottom] , 10 )
0 commit comments