Dekko
Loading...
Searching...
No Matches
MessageComposer.qml
1/* Copyright (C) 2016 - 2017 Dan Chapman <dpniel@ubuntu.com>
2 Copyright (C) 2025 CIT-Services <info@cit-services.eu>
3
4 This file is part of Dekko email client for Ubuntu devices
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18*/
19import QtQuick 2.4
20import QtQuick.Controls.Suru 2.2
21import Lomiri.Components 1.3
22import Lomiri.Components.Popups 1.3
23import Dekko.Components 1.0
24import Dekko.Mail 1.0
25import Dekko.Mail.API 1.0
26import Dekko.Mail.Stores.Composer 1.0
27import Dekko.Markdown 1.0
28import Dekko.Lomiri.Constants 1.0
29
30FocusScope {
31 anchors.fill: parent
32 property bool pendingHtmlToPlainConversion: false
33 property bool wasSwitchedToPlain: false
34
35 function plainTextToHtml(text) {
36 return '<p>' + (text || "")
37 .replace(/&/g, '&amp;')
38 .replace(/</g, '&lt;')
39 .replace(/>/g, '&gt;')
40 .replace(/"/g, '&quot;')
41 .replace(/'/g, '&#039;')
42 .replace(/\n/g, '<br>') + '</p>'
43 }
44
45 function hasValidHtmlContent(content) {
46 if (!content || content.trim().length === 0) {
47 return false
48 }
49 return content !== "<br>" && content !== "<p><br></p>" && content !== "<br/>"
50 }
51
52 Rectangle {
53 anchors.fill: parent
54 color: Suru.backgroundColor
55 }
56
57 Binding {
58 target: ComposerStore
59 property: "bodyDocument"
60 value: me.textDocument
61 }
62
63 Component.onCompleted: {
64 if (ComposerStore.composeMode === "html") {
65 // Force initial sync if opened directly in HTML mode
66 if (hasValidHtmlContent(ComposerStore.htmlContent)) {
67 htmlEditorContainer.setHtmlDocument(ComposerStore.htmlContent)
68 } else {
69 const plainText = me.text || ""
70 if (plainText.trim()) {
71 const wrappedHtml = plainTextToHtml(plainText)
72 htmlEditorContainer.setHtmlDocument(wrappedHtml)
73 ComposerStore.htmlContent = wrappedHtml
74 }
75 }
76 }
77 }
78
79 Component.onDestruction: {
80 pendingHtmlToPlainConversion = false
81 if (htmlEditorContainer) {
82 htmlEditorContainer.isEditorLoaded = false
83 }
84 }
85
86 Connections {
87 target: ComposerStore.sendInProgress ? null : ComposerStore
88 onComposeModeChanged: {
89 if (ComposerStore.sendInProgress) {
90 return;
91 }
92 if (ComposerStore.composeMode === "html") {
93 // Switched to HTML
94 pendingHtmlToPlainConversion = false
95
96 if (wasSwitchedToPlain) {
97 const plainText = me.text || ""
98 const wrappedHtml = plainTextToHtml(plainText)
99 htmlEditorContainer.setHtmlDocument(wrappedHtml)
100 ComposerStore.htmlContent = wrappedHtml
101 wasSwitchedToPlain = false
102 } else if (hasValidHtmlContent(ComposerStore.htmlContent)) {
103 htmlEditorContainer.setHtmlDocument(ComposerStore.htmlContent);
104 } else {
105 const plainText = me.text || ""
106 if (plainText.trim()) {
107 const wrappedHtml = plainTextToHtml(plainText)
108 htmlEditorContainer.setHtmlDocument(wrappedHtml)
109 ComposerStore.htmlContent = wrappedHtml
110 }
111 }
112 } else {
113 // Switched to plain text - convert HTML to plain text
114 wasSwitchedToPlain = true
115 if (hasValidHtmlContent(ComposerStore.htmlContent)) {
116 me.text = ComposerStore.builder.htmlToPlainText(ComposerStore.htmlContent)
117 }
118 if (htmlEditorContainer.editor) {
119 pendingHtmlToPlainConversion = true
120 htmlEditorContainer.editor.requestPlainText()
121 } else {
122 pendingHtmlToPlainConversion = false
123 }
124 }
125 }
126
127 onHtmlContentChanged: {
128 if (ComposerStore.sendInProgress) {
129 return;
130 }
131 if (ComposerStore.composeMode === "html" && htmlEditorContainer.visible) {
132 if (hasValidHtmlContent(ComposerStore.htmlContent)) {
133
134 if (!htmlEditorContainer.isInternallyUpdating && ComposerStore.htmlContent !== htmlEditorContainer.currentDocument) {
135 htmlEditorContainer.setHtmlDocument(ComposerStore.htmlContent)
136 }
137 }
138 }
139 }
140 }
141
142 Connections {
143 target: htmlEditorContainer.editor
144 onPlainTextReady: function(text) {
145 if (pendingHtmlToPlainConversion && ComposerStore.composeMode === "plain") {
146 const plain = (text || "").trim()
147 if (plain.length > 0 || !hasValidHtmlContent(ComposerStore.htmlContent)) {
148 me.text = plain
149 }
150 pendingHtmlToPlainConversion = false
151 }
152 }
153 }
154
155 Column {
156 anchors.fill: parent
157
158 // Persistent header - always visible
159 Column {
160 id: composerHeader
161 width: parent.width
162
164 anchors {
165 left: parent.left
166 right: parent.right
167 }
168 }
169
171 recipientType: RecipientType.To
172 recipientModel: ComposerStore.recipients.to
173 anchors {
174 left: parent.left
175 right: parent.right
176 }
177 }
178
180 visible: ComposerStore.showCC || ComposerStore.recipients.cc.count
181 recipientType: RecipientType.Cc
182 recipientModel: ComposerStore.recipients.cc
183 anchors {
184 left: parent.left
185 right: parent.right
186 }
187 }
188
190 visible: ComposerStore.showBCC || ComposerStore.recipients.bcc.count
191 recipientType: RecipientType.Bcc
192 recipientModel: ComposerStore.recipients.bcc
193 anchors {
194 left: parent.left
195 right: parent.right
196 }
197 }
198
200 anchors {
201 left: parent.left
202 right: parent.right
203 }
204 }
205 }
206
207 // Switchable editor area
208 Item {
209 width: parent.width
210 height: parent.height - composerHeader.height
211
212 MarkdownEditor {
213 id: me
214 anchors.fill: parent
215 frameSpacing: units.gu(2)
216 plainTextOnly: true
217 visible: ComposerStore.composeMode === "plain"
218 // No header - just the text area
219 }
220
222 id: htmlEditorContainer
223 anchors.fill: parent
224 visible: ComposerStore.composeMode === "html"
225 }
226 }
227 }
228}