mirror of
https://github.com/status-im/status-desktop.git
synced 2025-01-27 06:46:22 +00:00
bf14b06d55
This adds the UI plus all necessary models and signal handling to render discord import progress in the desktop application. It also introduces message handling for discord chat message types. Requires status-im/status-go#2826 to function Co-authored with @caybro
530 lines
21 KiB
QML
530 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) {
|
|
for (let i = 0; i < discordPages.length; i++) {
|
|
stackItems.push(discordPages[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
onClosed: {
|
|
if (root.isDiscordImport) {
|
|
root.store.clearFileList()
|
|
root.store.clearDiscordCategoriesAndChannels()
|
|
}
|
|
}
|
|
|
|
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
|
|
linkColor: hoveredLink ? Qt.lighter(Theme.palette.primaryColor1) : Theme.palette.primaryColor1
|
|
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
|
|
linkColor: hoveredLink ? Qt.lighter(Theme.palette.primaryColor1) : Theme.palette.primaryColor1
|
|
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
|
|
StatusCheckBox {
|
|
id: fileCheckBox
|
|
Layout.fillWidth: true
|
|
text: model.filePath
|
|
font.pixelSize: 13
|
|
checked: model.selected
|
|
enabled: model.errorMessage === "" // TODO distinguish between error/warning
|
|
onToggled: model.selected = checked
|
|
}
|
|
StatusBaseText {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: fileCheckBox.leftPadding + fileCheckBox.spacing + fileCheckBox.indicator.width
|
|
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
|
|
|
|
archiveSupportOptionVisible: root.store.isCommunityHistoryArchiveSupportEnabled
|
|
archiveSupportEnabled: archiveSupportOptionVisible
|
|
}
|
|
|
|
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
|
|
},
|
|
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
|
|
}
|
|
}
|