fix: create community and channel name validation

Fixes #2050.

This PR contains changes to fix the name validation for new communities and new channels in communities. In the process of updating this, better validation was also added to both popups (create community and create channel), including the prevention of the "Create" button from being enabled until all form fields were valid.

During this process, it was noticed that the community image cropper was not actually cropping the image *in the preview*. Once the community was created, status-go was successfully cropping the image as the user intended. However, the preview thumbnail prior to creation was not accurately showing the cropped image preview and showing the entire image centred instead. *This is still yet to be fixed.* One solution is to upgrade Qt to `5.15` to take advantage of Image QML's `sourceClipRect`.
This commit is contained in:
Eric Mastro 2021-03-29 23:28:41 +11:00 committed by Eric Mastro
parent 7eda54a141
commit 9e6bd7a2da
4 changed files with 222 additions and 93 deletions

View File

@ -10,6 +10,9 @@ ModalPopup {
property string communityId
readonly property int maxDescChars: 140
property string nameValidationError: ""
property bool isValid:
nameInput.isValid &&
descriptionTextArea.isValid
id: popup
height: 600
@ -18,22 +21,12 @@ ModalPopup {
nameInput.text = "";
nameInput.forceActiveFocus(Qt.MouseFocusReason)
}
onClosed: destroy()
function validate() {
nameValidationError = ""
if (nameInput.text === "") {
//% "You need to enter a name"
nameValidationError = qsTrId("you-need-to-enter-a-name")
} else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) {
//% "Please restrict your name to letters, numbers, dashes and spaces"
nameValidationError = qsTrId("please-restrict-your-name-to-letters--numbers--dashes-and-spaces")
} else if (nameInput.text.length > 100) {
//% "Your name needs to be 100 characters or shorter"
nameValidationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
}
return !nameValidationError && !descriptionTextArea.validationError
nameInput.validate()
descriptionTextArea.validate()
return isValid
}
//% "New channel"
@ -66,6 +59,30 @@ ModalPopup {
//% "A cool name"
placeholderText: qsTrId("a-cool-name")
validationError: popup.nameValidationError
property bool isValid: false
onTextEdited: {
if (text.includes(" ")) {
text = text.replace(" ", "-")
}
validate()
}
function validate() {
validationError = ""
if (nameInput.text === "") {
//% "You need to enter a name"
validationError = qsTrId("you-need-to-enter-a-name")
} else if (!(/^[a-z0-9\-]+$/.test(nameInput.text))) {
validationError = qsTr("Use only lowercase letters (a to z), numbers & dashes (-). Do not use chat keys.")
} else if (nameInput.text.length > 100) {
//% "Your name needs to be 100 characters or shorter"
validationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
}
isValid = validationError === ""
return validationError
}
}
StyledTextArea {
@ -75,10 +92,30 @@ ModalPopup {
//% "What your channel is about"
placeholderText: qsTrId("what-your-channel-is-about")
//% "The description cannot exceed %1 characters"
validationError: descriptionTextArea.text.length > maxDescChars ? qsTrId("the-description-cannot-exceed--1-characters").arg(maxDescChars) : ""
validationError: descriptionTextArea.text.length > popup.maxDescChars ? qsTrId("the-description-cannot-exceed-140-characters") :
popup.descriptionValidationError || ""
anchors.top: nameInput.bottom
anchors.topMargin: Style.current.bigPadding
customHeight: 88
property bool isValid: false
onTextChanged: validate()
function resetValidation() {
isValid = false
validationError = ""
}
function validate() {
validationError = ""
if (text.length > popup.maxDescChars) {
validationError = qsTrId("the-description-cannot-exceed-140-characters")
}
if (text === "") {
validationError = qsTr("You need to enter a description")
}
isValid = validationError === ""
}
}
StyledText {
@ -129,6 +166,7 @@ ModalPopup {
}
footer: StatusButton {
enabled: popup.isValid
//% "Create"
text: qsTrId("create")
anchors.right: parent.right

View File

@ -8,67 +8,34 @@ import "../../../../shared/status"
ModalPopup {
readonly property int maxDescChars: 140
property string nameValidationError: ""
property string descriptionValidationError: ""
property string colorValidationError: ""
property string selectedImageValidationError: ""
property string selectedImage: ""
property var imageDimensions: ({
aX: 0,
aY: 0,
bY: 1,
bY: 1
})
property QtObject community: chatsModel.communities.activeCommunity
property bool isEdit: false
property bool isValid:
nameInput.isValid &&
addImageButton.isValid &&
descriptionTextArea.isValid &&
colorPicker.isValid
id: popup
height: 600
onOpened: {
nameInput.text = isEdit ? community.name : "";
descriptionTextArea.text = isEdit ? community.description : "";
nameValidationError = "";
colorValidationError = "";
selectedImageValidationError = "";
if (isEdit) {
nameInput.text = community.name;
descriptionTextArea.text = community.description;
}
nameInput.forceActiveFocus(Qt.MouseFocusReason)
}
onClosed: destroy()
function validate() {
nameValidationError = ""
colorValidationError = ""
selectedImageValidationError = ""
descriptionValidationError = ""
if (nameInput.text === "") {
//% "You need to enter a name"
nameValidationError = qsTrId("you-need-to-enter-a-name")
} else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) {
//% "Please restrict your name to letters, numbers, dashes and spaces"
nameValidationError = qsTrId("please-restrict-your-name-to-letters--numbers--dashes-and-spaces")
} else if (nameInput.text.length > 100) {
//% "Your name needs to be 100 characters or shorter"
nameValidationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
}
if (descriptionTextArea.text === "") {
descriptionValidationError = qsTr("You need to enter a description")
}
if (selectedImage === "") {
//% "You need to select an image"
selectedImageValidationError = qsTrId("you-need-to-select-an-image")
}
if (colorPicker.text === "") {
colorValidationError = qsTr("You need to enter a color")
} else if (!Utils.isHexColor(colorPicker.text)) {
colorValidationError = qsTr("This field needs to be an hexadecimal color (eg: #4360DF)")
}
return !nameValidationError && !descriptionTextArea.validationError && !colorValidationError && !descriptionValidationError
nameInput.validate()
addImageButton.validate()
descriptionTextArea.validate()
colorPicker.validate()
return isValid
}
title: isEdit ?
@ -106,7 +73,29 @@ ModalPopup {
label: qsTrId("name-your-community")
//% "A catchy name"
placeholderText: qsTrId("name-your-community-placeholder")
validationError: popup.nameValidationError
property bool isValid: false
onTextEdited: {
if (text.includes(" ")) {
text = text.replace(" ", "-")
}
validate()
}
function validate() {
validationError = ""
if (nameInput.text === "") {
//% "You need to enter a name"
validationError = qsTrId("you-need-to-enter-a-name")
} else if (!(/^[a-z0-9\-]+$/.test(nameInput.text))) {
validationError = qsTr("Use only lowercase letters (a to z), numbers & dashes (-). Do not use chat keys.")
} else if (nameInput.text.length > 100) {
//% "Your name needs to be 100 characters or shorter"
validationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
}
isValid = validationError === ""
return validationError
}
}
StyledTextArea {
@ -116,18 +105,37 @@ ModalPopup {
//% "What your community is about"
placeholderText: qsTrId("what-your-community-is-about")
//% "The description cannot exceed 140 characters"
validationError: descriptionTextArea.text.length > maxDescChars ? qsTrId("the-description-cannot-exceed-140-characters") :
validationError: descriptionTextArea.text.length > popup.maxDescChars ? qsTrId("the-description-cannot-exceed-140-characters") :
popup.descriptionValidationError || ""
anchors.top: nameInput.bottom
anchors.topMargin: Style.current.bigPadding
customHeight: 88
textField.selectByMouse: true
textField.wrapMode: TextEdit.Wrap
property bool isValid: false
onTextChanged: validate()
function resetValidation() {
isValid = false
validationError = ""
}
function validate() {
validationError = ""
if (text.length > popup.maxDescChars) {
validationError = qsTrId("the-description-cannot-exceed-140-characters")
}
if (text === "") {
validationError = qsTr("You need to enter a description")
}
isValid = validationError === ""
}
}
StyledText {
id: charLimit
text: `${descriptionTextArea.text.length}/${maxDescChars}`
text: `${descriptionTextArea.text.length}/${popup.maxDescChars}`
anchors.top: descriptionTextArea.bottom
anchors.topMargin: !descriptionTextArea.validationError ? 5 : - Style.current.smallPadding
anchors.right: descriptionTextArea.right
@ -139,9 +147,11 @@ ModalPopup {
id: thumbnailText
//% "Thumbnail image"
text: qsTrId("thumbnail-image")
anchors.top: descriptionTextArea.bottom
anchors.top: charLimit.bottom
anchors.topMargin: Style.current.smallPadding
font.pixelSize: 15
font.pixelSize: 13
color: Style.current.textColor
font.weight: Font.Medium
}
@ -154,6 +164,23 @@ ModalPopup {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: thumbnailText.bottom
anchors.topMargin: Style.current.padding
property bool isValid: false
property string selectedImage: ""
onSelectedImageChanged: validate()
function resetValidation() {
isValid = false
imageValidation.text = ""
}
function validate() {
imageValidation.text = ""
if (selectedImage === "") {
imageValidation.text = qsTr("Please select an image")
}
isValid = imageValidation.text === ""
}
FileDialog {
id: imageDialog
@ -164,27 +191,38 @@ ModalPopup {
//% "Image files (*.jpg *.jpeg *.png)"
qsTrId("image-files----jpg---jpeg---png-")
]
onRejected: {
addImageButton.validate()
}
onAccepted: {
popup.selectedImage = imageDialog.fileUrls[0]
addImageButton.selectedImage = imageDialog.fileUrls[0]
imageCropperModal.open()
}
}
Image {
id: imagePreview
visible: !!popup.selectedImage
source: popup.selectedImage
fillMode: Image.PreserveAspectCrop
Rectangle {
id: imagePreviewCropper
clip: true
width: parent.width
height: parent.height
radius: parent.width / 2
visible: !!addImageButton.selectedImage
Image {
id: imagePreview
visible: !!addImageButton.selectedImage
source: addImageButton.selectedImage
fillMode: Image.PreserveAspectFit
width: parent.width
height: parent.height
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
anchors.centerIn: parent
width: imagePreview.width
height: imagePreview.height
radius: imagePreview.width / 2
width: imageCropperModal.width
height: imageCropperModal.height
radius: width / 2
}
}
}
@ -242,29 +280,52 @@ ModalPopup {
ImageCropperModal {
id: imageCropperModal
selectedImage: popup.selectedImage
selectedImage: addImageButton.selectedImage
ratio: "1:1"
onCropFinished: {
imageDimensions.aX = aX
imageDimensions.aY = aY
imageDimensions.bX = bX
imageDimensions.bY = bY
}
}
}
StyledText {
id: imageValidation
visible: text && text !== ""
anchors.top: addImageButton.bottom
anchors.topMargin: Style.current.smallPadding
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: Style.current.danger
}
Input {
property string defaultColor: "#4360DF"
property bool isValid: true
id: colorPicker
label: qsTr("Community color")
placeholderText: qsTr("Pick a color")
anchors.top: addImageButton.bottom
anchors.top: imageValidation.bottom
anchors.topMargin: Style.current.smallPadding
validationError: popup.colorValidationError
textField.text: defaultColor
textField.onReleased: colorDialog.open()
onTextChanged: validate()
function resetValidation() {
isValid = true
validationError = ""
}
function validate() {
validationError = ""
if (text === "") {
validationError = qsTr("Please enter a color")
} else if (!Utils.isHexColor(colorPicker.text)) {
validationError = qsTr("Must be an hexadecimal color (eg: #4360DF)")
}
isValid = validationError === ""
}
StatusIconButton {
icon.name: "caret"
iconRotation: -90
@ -368,6 +429,7 @@ ModalPopup {
}
footer: StatusButton {
enabled: popup.isValid
text: isEdit ?
//% "Edit"
qsTrId("edit") :
@ -389,11 +451,11 @@ ModalPopup {
membershipRequirementSettingPopup.checkedMembership,
ensOnlySwitch.switchChecked,
colorPicker.text,
popup.selectedImage,
imageDimensions.aX,
imageDimensions.aY,
imageDimensions.bX,
imageDimensions.bY)
addImageButton.selectedImage,
imageCropperModal.aX,
imageCropperModal.aY,
imageCropperModal.bX,
imageCropperModal.bY)
}
if (error) {

View File

@ -3,7 +3,8 @@ import "."
import "../imports"
Item {
property var image
id: root
property Image image
property alias selectorRectangle: selectorRectangle
property string ratio: ""
property var splitRatio: !!ratio ? ratio.split(":") : null
@ -11,6 +12,7 @@ Item {
property int heightRatio: !!ratio ? parseInt(splitRatio[1]) : -1
property bool settingCorners: false
property int draggedCorner: 0
property bool ready: false
readonly property int topLeft: 0
readonly property int topRight: 1
@ -96,6 +98,7 @@ Item {
if (image.status === Image.Ready) {
selectorRectangle.initialSetup()
selectorRectangle.visible = true
root.ready = true
}
}
}

View File

@ -6,7 +6,11 @@ import "./status"
ModalPopup {
property string selectedImage
property string ratio
property string ratio: "1:1"
property int aX: 0
property int aY: 0
property int bX: 0
property int bY: 0
signal cropFinished(aX: int, aY: int, bX: int, bY: int)
id: cropImageModal
@ -29,6 +33,28 @@ ModalPopup {
y: image.y
image: image
ratio: cropImageModal.ratio
onReadyChanged: {
if (ready) {
// cropImageModal.calculateCrop()
cropImageModal.aX = Qt.binding(function() {
const aXPercent = imageCropper.selectorRectangle.x / image.width
return Math.round(aXPercent * image.sourceSize.width)
})
cropImageModal.aY = Qt.binding(function() {
const aYPercent = imageCropper.selectorRectangle.y / image.height
return Math.round(aYPercent * image.sourceSize.height)
})
cropImageModal.bX = Qt.binding(function() {
const bXPercent = (imageCropper.selectorRectangle.x + imageCropper.selectorRectangle.width) / image.width
return Math.round(bXPercent * image.sourceSize.width)
})
cropImageModal.bY = Qt.binding(function() {
const bYPercent = (imageCropper.selectorRectangle.y + imageCropper.selectorRectangle.height) / image.height
return Math.round(bYPercent * image.sourceSize.height)
})
}
}
}
footer: StatusButton {