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 }