Warning, /network/tokodon/src/content/ui/StatusComposer/StatusComposer.qml is written in an unsupported language. File is not indexed.
0001 // SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu> 0002 // SPDX-FileCopyrightText: 2022 Joshua Goins <josh@redstrate.com> 0003 // SPDX-FileCopyrightText: 2022 Jeremy Winter <jeremy.winter@tutanota.com> 0004 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 0006 import QtCore 0007 import QtQuick 0008 import QtQuick.Controls 2 as QQC2 0009 import QtQuick.Layouts 0010 import QtQuick.Dialogs 0011 import org.kde.kirigami 2 as Kirigami 0012 import org.kde.kirigamiaddons.labs.components 1 as KirigamiComponents 0013 import org.kde.tokodon 0014 import '..' 0015 0016 Kirigami.ScrollablePage { 0017 id: root 0018 0019 enum Purpose { 0020 New, 0021 Reply, 0022 Redraft, 0023 Edit 0024 } 0025 0026 property var purpose 0027 property string inReplyTo: '' 0028 property var mentions: [] 0029 property int visibility: AccountManager.selectedAccount.preferences.defaultVisibility 0030 property int sensitive: AccountManager.selectedAccount.preferences.defaultSensitive 0031 readonly property NetworkRequestProgress progress: NetworkRequestProgress {} 0032 property var previewPost: null 0033 property string initialText 0034 property bool closeApplicationWhenFinished: false 0035 0036 readonly property PostEditorBackend defaultBackend: PostEditorBackend { 0037 inReplyTo: root.inReplyTo 0038 mentions: root.mentions 0039 visibility: root.visibility 0040 sensitive: root.sensitive 0041 } 0042 0043 property PostEditorBackend backend: defaultBackend 0044 0045 readonly property bool isPollValid: backend.pollEnabled ? backend.poll.isValid : true 0046 readonly property bool isStatusValid: textArea.text.length > 0 && backend.charactersLeft >= 0 0047 0048 title: { 0049 switch (root.purpose) { 0050 case StatusComposer.Edit: 0051 return i18n("Edit this post") 0052 case StatusComposer.Reply: 0053 return i18n("Reply to this post") 0054 case StatusComposer.Redraft: 0055 return i18n("Rewrite this post") 0056 case StatusComposer.New: 0057 return i18n("Write a new post") 0058 } 0059 } 0060 0061 data: Connections { 0062 target: backend 0063 function onPosted(error) { 0064 if (error.length === 0) { 0065 if (root.closeApplicationWhenFinished) { 0066 applicationWindow().close(); 0067 } else { 0068 applicationWindow().pageStack.layers.pop(); 0069 } 0070 applicationWindow().newPost(); 0071 } else { 0072 banner.text = error 0073 console.log(error); 0074 } 0075 } 0076 0077 function onEditComplete(obj) { 0078 applicationWindow().pageStack.layers.pop(); 0079 } 0080 } 0081 0082 Component.onCompleted: { 0083 if (initialText.length > 0) { 0084 backend.status = initialText 0085 } else if (root.purpose === StatusComposer.New || root.purpose === StatusComposer.Reply) { 0086 textArea.text = root.backend.mentions.filter((mention) => mention !== ('@' + AccountManager.selectedAccount.identity.account)).join(" ") 0087 } 0088 0089 textArea.forceActiveFocus() 0090 } 0091 0092 Kirigami.PromptDialog { 0093 id: discardDraftPrompt 0094 0095 title: i18nc("@title", "Discard Draft") 0096 subtitle: i18nc("@label", "Are you sure you want to discard your draft?") 0097 standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel 0098 showCloseButton: false 0099 0100 onAccepted: applicationWindow().pageStack.layers.pop(); 0101 } 0102 0103 onBackRequested: (event) => { 0104 if (textArea.text.length > 0) { 0105 discardDraftPrompt.open(); 0106 event.accepted = true; 0107 } 0108 } 0109 0110 function submitPost() { 0111 if(root.purpose === StatusComposer.Edit) { 0112 backend.edit() 0113 } else { 0114 backend.save() 0115 } 0116 } 0117 0118 function uploadFile(url) { 0119 progress.reply = backend.attachmentEditorModel.append(url); 0120 } 0121 0122 function pasteImage() { 0123 let localPath = Clipboard.saveImage(); 0124 if (localPath.length === 0) { 0125 return false; 0126 } 0127 uploadFile(localPath); 0128 return true; 0129 } 0130 0131 header: KirigamiComponents.Banner { 0132 id: banner 0133 Layout.fillWidth: true 0134 width: parent.width 0135 visible: text.length !== 0 0136 type: Kirigami.MessageType.Error 0137 } 0138 0139 Kirigami.FlexColumn { 0140 maximumWidth: Kirigami.Units.gridUnit * 40 0141 padding: 0 0142 0143 0144 QQC2.TextField { 0145 placeholderText: i18n("Content Warning") 0146 Layout.fillWidth: true 0147 visible: contentWarning.checked 0148 onTextChanged: root.backend.spoilerText = text 0149 } 0150 0151 Loader { 0152 active: root.previewPost !== null 0153 Layout.fillWidth: true 0154 sourceComponent: StatusPreview { 0155 post: root.previewPost 0156 } 0157 } 0158 0159 QQC2.TextArea { 0160 id: textArea 0161 placeholderText: i18n("What's new?") 0162 text: root.backend.status 0163 wrapMode: TextEdit.Wrap 0164 Layout.fillWidth: true 0165 Layout.fillHeight: true 0166 Layout.preferredHeight: Math.max(implicitHeight, Kirigami.Units.gridUnit * 12) + actions.implicitHeight 0167 bottomInset: -1 0168 leftInset: -1 0169 rightInset: -1 0170 onTextChanged: backend.status = text 0171 Kirigami.SpellCheck.enabled: true 0172 0173 Keys.onEnterPressed: (event)=> { 0174 if (event.modifiers & Qt.ControlModifier) { 0175 root.submitPost() 0176 event.accepted = true 0177 } else { 0178 event.accepted = false 0179 } 0180 } 0181 0182 Keys.onReturnPressed: (event)=> { 0183 if (event.modifiers & Qt.ControlModifier) { 0184 root.submitPost() 0185 event.accepted = true 0186 } else { 0187 event.accepted = false 0188 } 0189 } 0190 0191 Keys.onTabPressed: (event)=> { 0192 nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocusReason) 0193 event.accepted = true 0194 } 0195 0196 Keys.onPressed: (event) => { 0197 if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) { 0198 event.accepted = root.pasteImage() 0199 } 0200 } 0201 0202 DropArea { 0203 anchors.fill: parent 0204 enabled: !AccountManager.isFlatpak 0205 onDropped: (drop) => { 0206 for (let index in drop.urls) { 0207 root.uploadFile(drop.urls[index]) 0208 } 0209 } 0210 } 0211 0212 ColumnLayout { 0213 id: actions 0214 spacing: 0 0215 anchors { 0216 left: parent.left 0217 right: parent.right 0218 bottom: parent.bottom 0219 leftMargin: 1 0220 rightMargin: 1 0221 bottomMargin: 1 0222 } 0223 height: implicitHeight 0224 0225 Behavior on height { 0226 NumberAnimation { 0227 property: "height" 0228 duration: Kirigami.Units.shortDuration 0229 easing.type: Easing.OutCubic 0230 } 0231 } 0232 0233 EditorAttachmentGrid { 0234 attachmentEditorModel: root.backend.attachmentEditorModel 0235 Layout.fillWidth: true 0236 Layout.maximumWidth: actions.width - Layout.leftMargin - Layout.rightMargin - (columns > 1 ? Kirigami.Units.smallSpacing : 0) 0237 Layout.margins: Kirigami.Units.smallSpacing 0238 } 0239 0240 QQC2.ProgressBar { 0241 Layout.fillWidth: true 0242 from: 0 0243 to: 100 0244 visible: progress.uploading 0245 value: progress.progress 0246 indeterminate: progress.progress === 100 && progress.uploading 0247 Layout.leftMargin: Kirigami.Units.smallSpacing 0248 Layout.rightMargin: Kirigami.Units.smallSpacing 0249 } 0250 0251 Kirigami.Separator { 0252 visible: addPool.checked 0253 Layout.fillWidth: true 0254 } 0255 0256 ComposerPoll { 0257 visible: addPool.checked 0258 Layout.fillWidth: true 0259 0260 poll: backend.poll 0261 maxPollOptions: AccountManager.selectedAccount.maxPollOptions 0262 } 0263 0264 QQC2.ToolBar { 0265 id: actionsToolbar 0266 0267 Layout.fillWidth: true 0268 0269 RowLayout { 0270 QQC2.ToolButton { 0271 enabled: backend.attachmentEditorModel.count < 4 && !addPool.checked 0272 icon.name: "mail-attachment-symbolic" 0273 onClicked: fileDialog.open() 0274 FileDialog { 0275 id: fileDialog 0276 currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] 0277 title: i18n("Please choose a file") 0278 onAccepted: root.uploadFile(fileDialog.selectedFile); 0279 selectedNameFilter.index: 0 0280 nameFilters: [i18n("All supported formats (*.jpg *.jpeg *.png *.gif *.webp *.heic *.heif *.avif *.webm *.mp4 *.m4v *.mov)"), 0281 i18n("JPEG image (*.jpg *.jpeg)"), 0282 i18n("PNG image (*.png)"), 0283 i18n("GIF image (*.gif)"), 0284 i18n("WebP image (*.webp)"), 0285 i18n("HEIC image(*.heic)"), 0286 i18n("HEIF image (*.heif)"), 0287 i18n("AVIF image (*.avif)"), 0288 i18n("WebM video (*.webm)"), 0289 i18n("MPEG-4 video (*.mp4)"), 0290 i18n("M4V video (*.m4v)"), 0291 i18n("QuickTime video (*.mov)"), 0292 i18n("All files (*)")] 0293 } 0294 QQC2.ToolTip.text: i18n("Attach File") 0295 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay 0296 QQC2.ToolTip.visible: hovered 0297 } 0298 0299 QQC2.ToolButton { 0300 id: addPool 0301 icon.name: "gnumeric-graphguru" 0302 checkable: true 0303 enabled: backend.attachmentEditorModel.count === 0 && root.purpose !== StatusComposer.Edit 0304 QQC2.ToolTip.text: i18n("Add Poll") 0305 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay 0306 QQC2.ToolTip.visible: hovered 0307 0308 onToggled: backend.pollEnabled = checked 0309 } 0310 0311 QQC2.ToolButton { 0312 icon.name: { 0313 switch(backend.visibility) { 0314 case Post.Public: 0315 return "kstars_xplanet"; 0316 case Post.Unlisted: 0317 return "unlock"; 0318 case Post.Private: 0319 return "lock"; 0320 case Post.Direct: 0321 return "mail-message"; 0322 case Post.Local: 0323 return "group"; 0324 default: 0325 return "kstars_xplanet"; 0326 } 0327 } 0328 onClicked: visibilityMenu.open() 0329 enabled: root.purpose !== StatusComposer.Edit 0330 QQC2.Menu { 0331 id: visibilityMenu 0332 QQC2.MenuItem { 0333 icon.name: "group" 0334 text: i18n("Local") 0335 onTriggered: backend.visibility = Post.Local 0336 visible: AccountManager.selectedAccount.supportsLocalVisibility 0337 } 0338 QQC2.MenuItem { 0339 icon.name: "kstars_xplanet" 0340 text: i18n("Public") 0341 onTriggered: backend.visibility = Post.Public 0342 } 0343 QQC2.MenuItem { 0344 icon.name: "unlock" 0345 text: i18n("Unlisted") 0346 onTriggered: backend.visibility = Post.Unlisted 0347 } 0348 QQC2.MenuItem { 0349 icon.name: "lock" 0350 text: i18n("Private") 0351 onTriggered: backend.visibility = Post.Private 0352 } 0353 QQC2.MenuItem { 0354 icon.name: "mail-message" 0355 text: i18n("Direct Message") 0356 onTriggered: backend.visibility = Post.Direct 0357 } 0358 } 0359 QQC2.ToolTip { 0360 text: i18n("Visibility") 0361 } 0362 } 0363 QQC2.ToolButton { 0364 id: contentWarning 0365 text: i18nc("Short for content warning", "cw") 0366 checkable: true 0367 QQC2.ToolTip { 0368 text: i18n("Content Warning") 0369 } 0370 } 0371 QQC2.ToolButton { 0372 id: languageButton 0373 text: backend.language 0374 QQC2.ToolTip.text: i18n("Post Language") 0375 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay 0376 QQC2.ToolTip.visible: hovered 0377 checkable: true 0378 0379 onClicked: languageSelect.open() 0380 LanguageSelector { 0381 id: languageSelect 0382 0383 onAboutToShow: { 0384 const sourceIndex = listView.model.sourceModel.indexOfValue(backend.language); 0385 listView.currentIndex = listView.model.mapFromSource(sourceIndex).row; 0386 } 0387 onCodeSelected: code => backend.language = code 0388 } 0389 } 0390 QQC2.ToolButton { 0391 id: emojiButton 0392 0393 icon.name: "smiley" 0394 onClicked: emojiDialog.open() 0395 EmojiDialog { 0396 id: emojiDialog 0397 0398 modal: false 0399 0400 onChosen: (emoji) => textArea.insert(textArea.cursorPosition, emoji) 0401 onClosed: if (emojiButton.checked) emojiButton.checked = false 0402 } 0403 QQC2.ToolTip.text: i18n("Add Emoji") 0404 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay 0405 QQC2.ToolTip.visible: hovered 0406 } 0407 } 0408 } 0409 } 0410 } 0411 0412 RowLayout { 0413 Layout.fillWidth: true 0414 0415 QQC2.Label { 0416 Layout.fillHeight: true 0417 Layout.alignment: Qt.AlignLeft 0418 verticalAlignment: Text.AlignVCenter 0419 text: i18nc("@label Character count in the status composer", "<b>%1/%2</b> characters", root.backend.charactersLeft, AccountManager.selectedAccount.maxPostLength) 0420 } 0421 0422 Item { 0423 Layout.fillWidth: true 0424 } 0425 0426 QQC2.Button { 0427 icon.name: { 0428 switch (root.purpose) { 0429 case StatusComposer.New: 0430 return "document-send-symbolic"; 0431 case StatusComposer.Reply: 0432 return "tokodon-post-reply"; 0433 case StatusComposer.Redraft: 0434 return "edit-redo-symbolic"; 0435 case StatusComposer.Edit: 0436 return "document-edit-symbolic"; 0437 } 0438 } 0439 text: { 0440 switch (root.purpose) { 0441 case StatusComposer.New: 0442 return i18nc("@action:button Send a post", "Send"); 0443 case StatusComposer.Reply: 0444 return i18nc("@action:button Reply to a post", "Reply"); 0445 case StatusComposer.Redraft: 0446 return i18nc("@action:button Send the same post again", "Repost"); 0447 case StatusComposer.Edit: 0448 return i18nc("@action:Button Edit a post", "Edit"); 0449 } 0450 } 0451 enabled: root.isStatusValid && root.isPollValid && (!progress.uploading || backend.attachmentEditorModel.count > 0) 0452 Layout.alignment: Qt.AlignRight 0453 onClicked: root.submitPost() 0454 } 0455 } 0456 } 0457 0458 background: Rectangle { 0459 color: Kirigami.Theme.backgroundColor 0460 Kirigami.Theme.colorSet: Kirigami.Theme.Window 0461 Image { 0462 anchors.left: parent.left 0463 anchors.bottom: parent.bottom 0464 source: "qrc:/content/elephant.svg" 0465 } 0466 } 0467 }