status-desktop/ui/app/AppLayouts/CommunitiesPortal/popups/CreateCommunityPopup.qml

541 lines
21 KiB
QML

import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtQuick.Dialogs 1.3
import utils 1.0
import shared.panels 1.0
import shared.popups 1.0
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Popups 0.1
import "../../Chat/controls/community"
import "../controls"
import "../panels"
StatusStackModal {
id: root
property var store
property bool isDiscordImport // creating new or importing from discord?
stackTitle: isDiscordImport ? qsTr("Import a community from Discord into Status") :
qsTr("Create New Community")
width: 640
nextButton: StatusButton {
objectName: "createCommunityNextBtn"
font.weight: Font.Medium
text: typeof currentItem.nextButtonText !== "undefined" ? currentItem.nextButtonText : qsTr("Next")
enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext
loading: root.store.discordDataExtractionInProgress
onClicked: {
let nextAction = currentItem.nextAction
if (typeof(nextAction) == "function") {
return nextAction()
}
root.currentIndex++
}
}
finishButton: StatusButton {
objectName: "createCommunityFinalBtn"
font.weight: Font.Medium
text: root.isDiscordImport ? qsTr("Start Discord import") : qsTr("Create Community")
enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext
onClicked: {
let nextAction = currentItem.nextAction
if (typeof (nextAction) == "function") {
return nextAction()
}
if (!root.isDiscordImport)
d.createCommunity()
}
}
readonly property var clearFilesButton: StatusButton {
font.weight: Font.Medium
text: qsTr("Clear all")
type: StatusBaseButton.Type.Danger
visible: root.currentItem.objectName === "discordFileListView" // no better way to address the current item in the stack :/
enabled: !fileListView.fileListModelEmpty && !root.store.discordDataExtractionInProgress
onClicked: root.store.clearFileList()
}
rightButtons: [clearFilesButton, nextButton, finishButton]
onAboutToShow: {
nameInput.input.edit.forceActiveFocus()
if (root.isDiscordImport) {
if (!root.store.discordImportInProgress) {
root.store.clearFileList()
root.store.clearDiscordCategoriesAndChannels()
}
for (let i = 0; i < discordPages.length; i++) {
stackItems.push(discordPages[i])
}
}
}
readonly property list<Item> discordPages: [
ColumnLayout {
id: fileListView
objectName: "discordFileListView" // !!! DON'T CHANGE, clearFilesButton depends on this
spacing: 24
readonly property var fileListModel: root.store.discordFileList
readonly property bool fileListModelEmpty: !fileListModel.count
readonly property bool canGoNext: fileListModel.selectedCount
|| (fileListModel.selectedCount && fileListModel.selectedFilesValid)
readonly property string nextButtonText:
fileListModel.selectedCount && fileListModel.selectedFilesValid ? qsTr("Proceed with (%1/%2) files").arg(fileListModel.selectedCount).arg(fileListModel.count) :
fileListModel.selectedCount ? qsTr("Validate (%1/%2) files").arg(fileListModel.selectedCount).arg(fileListModel.count)
: qsTr("Import files")
readonly property var nextAction: function () {
if (!fileListView.fileListModel.selectedFilesValid) {
return root.store.requestExtractChannelsAndCategories()
}
root.currentIndex++
}
RowLayout {
Layout.fillWidth: true
spacing: 12
StatusBaseText {
font.pixelSize: 15
text: fileListView.fileListModelEmpty ? qsTr("Select Discord JSON files to import") :
root.store.discordImportErrorsCount ? qsTr("Some of your community files cannot be used") :
qsTr("Uncheck any files you would like to exclude from the import")
}
StatusBaseText {
visible: fileListView.fileListModelEmpty
font.pixelSize: 12
color: Theme.palette.baseColor1
text: qsTr("(JSON file format only)")
}
IssuePill {
type: root.store.discordImportErrorsCount ? IssuePill.Type.Error : IssuePill.Type.Warning
count: {
if (root.store.discordImportErrorsCount > 0) {
return root.store.discordImportErrorsCount
}
if (root.store.discordImportWarningsCount > 0) {
return root.store.discordImportWarningsCount
}
return 0
}
visible: !!count
}
Item { Layout.fillWidth: true }
StatusButton {
text: qsTr("Browse files")
type: StatusBaseButton.Type.Primary
onClicked: fileDialog.open()
enabled: !root.store.discordDataExtractionInProgress
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.palette.baseColor4
ColumnLayout {
visible: fileListView.fileListModelEmpty
anchors.top: parent.top
anchors.topMargin: 60
anchors.horizontalCenter: parent.horizontalCenter
spacing: 8
StatusRoundIcon {
Layout.alignment: Qt.AlignHCenter
asset.name: "info"
}
StatusBaseText {
Layout.topMargin: 8
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Qt.AlignHCenter
text: qsTr("Export your Discord JSON data using %1")
.arg("<a href='https://github.com/Tyrrrz/DiscordChatExporter'>DiscordChatExporter</a>")
onLinkActivated: Global.openLink(link)
HoverHandler {
id: handler1
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: handler1.hovered && parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
StatusBaseText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Qt.AlignHCenter
text: qsTr("Refer to this <a href='https://github.com/Tyrrrz/DiscordChatExporter/wiki'>wiki</a> if you have any queries")
onLinkActivated: Global.openLink(link)
HoverHandler {
id: handler2
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: handler2.hovered && parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
StatusListView {
visible: !fileListView.fileListModelEmpty
enabled: !root.store.discordDataExtractionInProgress
anchors.fill: parent
anchors.margins: 16
model: fileListView.fileListModel
delegate: ColumnLayout {
width: ListView.view.width
RowLayout {
spacing: 20
Layout.fillWidth: true
Layout.topMargin: 8
StatusBaseText {
Layout.fillWidth: true
text: model.filePath
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.WordWrap
maximumLineCount: 2
}
StatusFlatRoundButton {
id: removeButton
Layout.preferredWidth: 32
Layout.preferredHeight: 32
type: StatusFlatRoundButton.Type.Secondary
icon.name: "close"
icon.color: Theme.palette.directColor1
icon.width: 24
icon.height: 24
onClicked: root.store.removeFileListItem(model.filePath)
}
}
StatusBaseText {
Layout.fillWidth: true
text: "%1 %2".arg("⚠").arg(model.errorMessage)
visible: model.errorMessage
font.pixelSize: 13
font.weight: Font.Medium
elide: Text.ElideMiddle
color: Theme.palette.dangerColor1
verticalAlignment: Qt.AlignTop
}
}
}
}
FileDialog {
id: fileDialog
title: qsTr("Choose files to import")
selectMultiple: true
nameFilters: [qsTr("JSON files (%1)").arg("*.json")]
onAccepted: {
if (fileDialog.fileUrls.length > 0) {
let files = []
for (let i = 0; i < fileDialog.fileUrls.length; i++)
files.push(decodeURI(fileDialog.fileUrls[i].toString()))
root.store.setFileListItems(files)
}
}
}
},
ColumnLayout {
id: categoriesAndChannelsView
spacing: 24
readonly property bool canGoNext: root.store.discordChannelsModel.hasSelectedItems
readonly property var nextAction: function () {
d.requestImportDiscordCommunity()
// replace ourselves with the progress dialog, no way back
root.leftButtons[0].visible = false
root.backgroundColor = Theme.palette.baseColor4
root.replace(progressComponent)
}
Component {
id: progressComponent
DiscordImportProgressContents {
width: root.availableWidth
store: root.store
onClose: root.close()
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !root.store.discordChannelsModel.count
Loader {
anchors.centerIn: parent
active: parent.visible
sourceComponent: StatusLoadingIndicator {
width: 50
height: 50
}
}
}
ColumnLayout {
spacing: 12
visible: root.store.discordChannelsModel.count
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Please select the categories and channels you would like to import")
wrapMode: Text.WordWrap
}
RowLayout {
spacing: 20
Layout.fillWidth: true
StatusRadioButton {
text: qsTr("Import all history")
checked: true
}
StatusRadioButton {
id: startDateRadio
text: qsTr("Start date")
}
StatusDatePicker {
id: datePicker
Layout.fillWidth: true
selectedDate: new Date(root.store.discordOldestMessageTimestamp * 1000)
enabled: startDateRadio.checked
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.palette.baseColor4
StatusListView {
anchors.fill: parent
anchors.margins: 16
model: root.store.discordCategoriesModel
delegate: ColumnLayout {
width: ListView.view.width
spacing: 8
StatusCheckBox {
readonly property string categoryId: model.id
id: categoryCheckbox
checked: model.selected
text: model.name
onToggled: root.store.toggleDiscordCategory(categoryId, checked)
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.leftMargin: 24
Repeater {
Layout.fillWidth: true
model: root.store.discordChannelsModel
delegate: StatusCheckBox {
width: parent.width
text: model.name
checked: model.selected
visible: model.categoryId === categoryCheckbox.categoryId
onToggled: root.store.toggleDiscordChannel(model.id, checked)
}
}
}
}
}
}
}
}
]
stackItems: [
StatusScrollView {
id: generalView
readonly property bool canGoNext: nameInput.valid && descriptionTextInput.valid
clip: false
ColumnLayout {
id: generalViewLayout
width: generalView.availableWidth
spacing: 16
CommunityNameInput {
id: nameInput
input.edit.objectName: "createCommunityNameInput"
Layout.fillWidth: true
input.tabNavItem: descriptionTextInput.input.edit
}
CommunityDescriptionInput {
id: descriptionTextInput
input.edit.objectName: "createCommunityDescriptionInput"
Layout.fillWidth: true
input.tabNavItem: nameInput.input.edit
}
CommunityLogoPicker {
id: logoPicker
Layout.fillWidth: true
}
CommunityBannerPicker {
id: bannerPicker
Layout.fillWidth: true
}
CommunityColorPicker {
id: colorPicker
onPick: root.replace(colorPanel)
Layout.fillWidth: true
Component {
id: colorPanel
CommunityColorPanel {
Component.onCompleted: color = colorPicker.color
onAccepted: {
colorPicker.color = color;
root.replace(null);
}
}
}
}
CommunityTagsPicker {
id: communityTagsPicker
tags: root.store.communityTags
onPick: root.replace(tagsPanel)
Layout.fillWidth: true
Component {
id: tagsPanel
CommunityTagsPanel {
Component.onCompleted: {
tags = communityTagsPicker.tags;
selectedTags = communityTagsPicker.selectedTags;
}
onAccepted: {
communityTagsPicker.selectedTags = selectedTags;
root.replace(null);
}
}
}
}
StatusModalDivider {
Layout.fillWidth: true
}
CommunityOptions {
id: options
Layout.fillWidth: true
}
Item {
Layout.fillHeight: true
}
}
},
ColumnLayout {
id: introOutroMessageView
spacing: 11
readonly property bool canGoNext: introMessageInput.valid && outroMessageInput.valid
CommunityIntroMessageInput {
id: introMessageInput
input.edit.objectName: "createCommunityIntroMessageInput"
input.tabNavItem: outroMessageInput.input.edit
Layout.fillWidth: true
Layout.fillHeight: true
minimumHeight: height
maximumHeight: (height - Style.current.xlPadding)
}
CommunityOutroMessageInput {
id: outroMessageInput
input.edit.objectName: "createCommunityOutroMessageInput"
input.tabNavItem: introMessageInput.input.edit
Layout.fillWidth: true
}
}
]
QtObject {
id: d
function _getCommunityConfig() {
return {
name: StatusQUtils.Utils.filterXSS(nameInput.input.text),
description: StatusQUtils.Utils.filterXSS(descriptionTextInput.input.text),
introMessage: StatusQUtils.Utils.filterXSS(introMessageInput.input.text),
outroMessage: StatusQUtils.Utils.filterXSS(outroMessageInput.input.text),
color: colorPicker.color.toString().toUpperCase(),
tags: communityTagsPicker.selectedTags,
image: {
src: logoPicker.source,
AX: logoPicker.cropRect.x,
AY: logoPicker.cropRect.y,
BX: logoPicker.cropRect.x + logoPicker.cropRect.width,
BY: logoPicker.cropRect.y + logoPicker.cropRect.height,
},
options: {
historyArchiveSupportEnabled: options.archiveSupportEnabled,
checkedMembership: options.requestToJoinEnabled ? Constants.communityChatOnRequestAccess : Constants.communityChatPublicAccess,
pinMessagesAllowedForMembers: options.pinMessagesEnabled,
encrypted: options.requestToJoinEnabled && options.encrypted // Only communities with memberships can be encrypted
},
bannerJsonStr: JSON.stringify({imagePath: String(bannerPicker.source).replace("file://", ""), cropRect: bannerPicker.cropRect})
}
}
function createCommunity() {
const error = root.store.createCommunity(_getCommunityConfig())
if (error) {
errorDialog.text = error.error
errorDialog.open()
}
root.close()
}
function requestImportDiscordCommunity() {
const error = root.store.requestImportDiscordCommunity(_getCommunityConfig(), datePicker.selectedDate.valueOf()/1000)
if (error) {
errorDialog.text = error.error
errorDialog.open()
}
}
}
MessageDialog {
id: errorDialog
title: qsTr("Error creating the community")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}