feat(Profile): New Social Links workflow

TLDR;
- all links visible in settings/popup
- persistent order of items
- drag'n'drop to reorder
- editing/deleting in a new popup
- several links of the same type

Needs changes in nimqml (to expose QAIM::begin/endMoveRows), in
DOtherSide (to expose those to NIM), in status-go (to preserve the links
order and fully save them to DB)

Fixes #9777
This commit is contained in:
Lukáš Tinkl 2023-03-08 02:56:41 +01:00 committed by Lukáš Tinkl
parent 494ab84fe1
commit 1998a6556a
38 changed files with 1096 additions and 428 deletions

View File

@ -48,18 +48,9 @@ method isLoaded*(self: Module): bool =
method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant
func hasCustomLink(socialLinks: seq[SocialLinkItem]): bool =
for socialLinkItem in socialLinks:
if socialLinkItem.linkType() == LinkType.Custom:
return true
method viewDidLoad*(self: Module) =
var socialLinkItems = toSocialLinkItems(self.controller.getSocialLinks())
# Add custom link placeholder
if not socialLinkItems.hasCustomLink:
socialLinkItems.add(initSocialLinkItem("", "", LinkType.Custom))
self.view.socialLinksModel().setItems(socialLinkItems)
self.view.temporarySocialLinksModel().setItems(socialLinkItems)

View File

@ -90,35 +90,38 @@ QtObject:
read = areSocialLinksDirty
notify = socialLinksDirtyChanged
proc createCustomLink(self: View, text: string, url: string) {.slot.} =
self.temporarySocialLinksModel.appendItem(initSocialLinkItem(text, url, LinkType.Custom))
proc createLink(self: View, text: string, url: string, linkType: int, icon: string) {.slot.} =
self.temporarySocialLinksModel.appendItem(initSocialLinkItem(text, url, (LinkType)linkType, icon))
self.temporarySocialLinksJsonChanged()
self.socialLinksDirtyChanged()
proc removeCustomLink(self: View, uuid: string) {.slot.} =
proc removeLink(self: View, uuid: string) {.slot.} =
if (self.temporarySocialLinksModel.removeItem(uuid)):
self.temporarySocialLinksJsonChanged()
self.socialLinksDirtyChanged()
proc updateLink(self: View, uuid: string, text: string, url: string): bool {.slot.} =
proc updateLink(self: View, uuid: string, text: string, url: string) {.slot.} =
if (self.temporarySocialLinksModel.updateItem(uuid, text, url)):
self.temporarySocialLinksJsonChanged()
self.socialLinksDirtyChanged()
proc resetSocialLinks(self: View): bool {.slot.} =
proc moveLink(self: View, fromRow: int, toRow: int) {.slot.} =
discard self.temporarySocialLinksModel.moveItem(fromRow, toRow)
proc resetSocialLinks(self: View) {.slot.} =
if (self.areSocialLinksDirty()):
self.temporarySocialLinksModel.setItems(self.socialLinksModel.items)
self.socialLinksDirtyChanged()
self.temporarySocialLinksJsonChanged()
proc saveSocialLinks(self: View): bool {.slot.} =
proc saveSocialLinks(self: View, silent: bool = false): bool {.slot.} =
result = self.delegate.saveSocialLinks()
if (result):
self.socialLinksModel.setItems(self.temporarySocialLinksModel.items)
self.socialLinksDirtyChanged()
self.socialLinksJsonChanged()
self.temporarySocialLinksJsonChanged()
if not silent:
self.socialLinksDirtyChanged()
proc bioChanged*(self: View) {.signal.}
proc getBio(self: View): string {.slot.} =

View File

@ -45,21 +45,44 @@ QtObject:
self.endResetModel()
proc appendItem*(self: SocialLinksModel, item: SocialLinkItem) =
self.beginInsertRows(newQModelIndex(), self.items.len, self.items.len)
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginInsertRows(parentModelIndex, self.items.len, self.items.len)
self.items.add(item)
self.endInsertRows()
proc removeItem*(self: SocialLinksModel, uuid: string): bool =
for i in 0 ..< self.items.len:
if (self.items[i].uuid == uuid):
if (self.items[i].linkType == LinkType.Custom):
self.beginRemoveRows(newQModelIndex(), i, i)
self.items.delete(i)
self.endRemoveRows()
return true
return false
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginRemoveRows(parentModelIndex, i, i)
self.items.delete(i)
self.endRemoveRows()
return true
return false
# used only by the temp model to reorder items with DND; compat with ListModel::move(from, to, count)
proc moveItem*(self: SocialLinksModel, fromRow: int, toRow: int): bool =
if toRow < 0 or toRow > self.items.len - 1:
return false
let sourceIndex = newQModelIndex()
defer: sourceIndex.delete
let destIndex = newQModelIndex()
defer: destIndex.delete
var destRow = toRow
if toRow > fromRow:
inc(destRow)
let currentItem = self.items[fromRow]
self.beginMoveRows(sourceIndex, fromRow, fromRow, destIndex, destRow)
self.items.delete(fromRow)
self.items.insert(@[currentItem], toRow)
self.endMoveRows()
return true
proc updateItem*(self: SocialLinksModel, uuid, text, url: string): bool =
for i in 0 ..< self.items.len:
if (self.items[i].uuid == uuid):
@ -75,6 +98,7 @@ QtObject:
if changedRoles.len > 0:
let index = self.createIndex(i, 0, nil)
defer: index.delete
self.dataChanged(index, index, changedRoles)
return true

View File

@ -22,7 +22,7 @@ proc socialLinkTextToIcon(text: string): string =
if (text == SOCIAL_LINK_YOUTUBE_ID): return "youtube"
if (text == SOCIAL_LINK_DISCORD_ID): return "discord"
if (text == SOCIAL_LINK_TELEGRAM_ID): return "telegram"
return ""
return "link"
proc toSocialLinks*(jsonObj: JsonNode): SocialLinks =
result = map(jsonObj.getElems(),

View File

@ -153,4 +153,8 @@ ListModel {
title: "LanguageCurrencySettings"
section: "Settings"
}
ListElement {
title: "ProfileSocialLinksPanel"
section: "Panels"
}
}

View File

@ -124,5 +124,8 @@
],
"CommunityMintedTokensView": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2934%3A479136&t=zs22ORYUVDYpqubQ-1"
],
"ProfileSocialLinksPanel": [
"https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=14588%3A308727&t=cwFGbBHsAGOP0T5R-0"
]
}

View File

@ -0,0 +1,142 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 as CoreUtils
import mainui 1.0
import AppLayouts.Profile.panels 1.0
import utils 1.0
import Storybook 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Vertical
Popups {
popupParent: root
rootStore: QtObject {}
}
ListModel {
id: linksModel
ListElement {
uuid: "0001"
text: "__github"
url: "https://github.com/caybro"
linkType: 3 // Constants.socialLinkType.github
icon: "github"
}
ListElement {
uuid: "0002"
text: "__twitter"
url: "https://twitter.com/caybro"
linkType: 1 // Constants.socialLinkType.twitter
icon: "twitter"
}
ListElement {
uuid: "0003"
text: "__personal_site"
url: "https://status.im"
linkType: 2 // Constants.socialLinkType.personalSite
icon: "language"
}
ListElement {
uuid: "0004"
text: "__youtube"
url: "https://www.youtube.com/@LukasTinkl"
linkType: 4 // Constants.socialLinkType.youtube
icon: "youtube"
}
ListElement { // NB: empty on purpose, for testing
uuid: ""
text: ""
url: ""
linkType: -1
icon: ""
}
ListElement {
uuid: "0005"
text: "Figma design very long URL link text that should elide"
url: "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1223%3A124882&t=qvYeJ8grsZLyUS0V-0"
linkType: 0 // Constants.socialLinkType.custom
icon: "link"
}
ListElement {
uuid: "0006"
text: "__telegram"
url: "https://t.me/ltinkl"
linkType: 6 // Constants.socialLinkType.telegram
icon: "telegram"
}
}
Connections {
target: Global
function onOpenLink(link) {
logs.logEvent("Global::openLink", ["link"], [link])
}
}
StatusScrollView { // wrapped in a ScrollView on purpose; to simulate SettingsContentBase.qml
SplitView.fillWidth: true
SplitView.preferredHeight: 300
ProfileSocialLinksPanel {
width: 500
profileStore: QtObject {
function createLink(text, url, linkType, icon) {
logs.logEvent("ProfileStore::createLink", ["text", "url", "linkType", "icon"], arguments)
linksModel.append({text, url, linkType, icon})
}
function removeLink(uuid) {
logs.logEvent("ProfileStore::removeLink", ["uuid"], arguments)
const idx = CoreUtils.ModelUtils.indexOf(linksModel, "uuid", uuid)
if (idx === -1)
return
linksModel.remove(idx, 1)
}
function updateLink(uuid, text, url) {
logs.logEvent("ProfileStore::updateLink", ["uuid", "text", "url"], arguments)
const idx = CoreUtils.ModelUtils.indexOf(linksModel, "uuid", uuid)
if (idx === -1)
return
if (!!text)
linksModel.setProperty(idx, "text", text)
if (!!url)
linksModel.setProperty(idx, "url", url)
}
function moveLink(fromRow, toRow, count) {
logs.logEvent("ProfileStore::moveLink", ["fromRow", "toRow", "count"], arguments)
linksModel.move(fromRow, toRow, 1)
}
function resetSocialLinks() {
logs.logEvent("ProfileStore::resetSocialLinks")
}
function saveSocialLinks(silent = false) {
logs.logEvent("ProfileStore::saveSocialLinks", ["silent"], arguments)
}
}
socialLinksModel: linksModel
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 200
logsView.logText: logs.logText
}
}

View File

@ -9,6 +9,7 @@ Feature: User Identity
And the user signs up with username "tester123" and password "TesTEr16843/!@00"
And the user lands on the signed in app
@mayfail
Scenario Outline: The user sets display name, bio and social links
Given the user opens app settings screen
And the user opens the profile settings

View File

@ -0,0 +1,240 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
/*!
\qmltype StatusDraggableListItem
\inherits QtQuickControls::ItemDelegate
\inqmlmodule StatusQ.Components
\since StatusQ.Components 0.1
\brief It is a list item with the ability to be dragged and dropped to reorder within a list view. Inherits from \c QtQuickControls::ItemDelegate.
The \c StatusDraggableListItem is a list item with an icon, title and a subtitle on the left side, and optional actions on the right.
It displays a drag handle on its left side
Example of how to use it:
\qml
StatusListView {
id: linksView
Layout.fillWidth: true
Layout.fillHeight: true
model: root.socialLinksModel
displaced: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
}
delegate: DropArea {
id: delegateRoot
property int visualIndex: index
width: ListView.view.width
height: draggableDelegate.height
keys: ["x-status-draggable-list-item-internal"]
onEntered: function(drag) {
const from = drag.source.visualIndex
const to = draggableDelegate.visualIndex
if (to === from)
return
root.profileStore.moveLink(from, to, 1)
drag.accept()
}
onDropped: function(drop) {
root.profileStore.saveSocialLinks(true)
}
StatusDraggableListItem {
id: draggableDelegate
readonly property string asideText: ProfileUtils.stripSocialLinkPrefix(model.url, model.linkType)
visible: !!asideText
width: parent.width
height: visible ? implicitHeight : 0
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
dragParent: linksView
visualIndex: delegateRoot.visualIndex
draggable: linksView.count > 1
title: ProfileUtils.linkTypeToText(model.linkType) || model.text
icon.name: model.icon
icon.color: ProfileUtils.linkTypeColor(model.linkType)
actions: [
StatusLinkText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
horizontalAlignment: Text.AlignRight
font.pixelSize: Theme.primaryTextFontSize
font.weight: Font.Normal
text: draggableDelegate.asideText
onClicked: Global.openLink(model.url)
},
StatusFlatRoundButton {
icon.name: "edit_pencil"
Layout.preferredWidth: 24
Layout.preferredHeight: 24
icon.width: 16
icon.height: 16
type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Edit link")
onClicked: Global.openPopup(modifySocialLinkModal,
{linkType: model.linkType, icon: model.icon, uuid: model.uuid,
linkText: model.text, linkUrl: draggableDelegate.asideText})
}
]
}
}
}
\endqml
For a list of components available see StatusQ.
*/
ItemDelegate {
id: root
/*!
\qmlproperty string StatusDraggableListItem::title
This property holds the primary text (title)
*/
property string title: text
/*!
\qmlproperty string StatusDraggableListItem::secondaryTitle
This property holds the secondary text (title), displayed below primary
*/
property string secondaryTitle
/*!
\qmlproperty list<Item> StatusDraggableListItem::actions
This property holds the optional list of actions, displayed on the right side.
The actions are reparented into a RowLayout.
*/
property list<Item> actions
onActionsChanged: {
for (let idx in actions) {
let action = actions[idx]
action.parent = actionsRow
}
}
/*!
\qmlproperty Item StatusDraggableListItem::dragParent
This property holds the drag parent (the Item that this Item gets reparented to when being dragged)
*/
property Item dragParent
/*!
\qmlproperty int StatusDraggableListItem::visualIndex
This property holds the persistent visual index of this item's parent (usually a DropArea)
*/
property int visualIndex
/*!
\qmlproperty bool StatusDraggableListItem::draggable
This property holds whether this item can be dragged (and whether the drag handle is displayed)
*/
property bool draggable
Drag.dragType: Drag.Automatic
Drag.hotSpot.x: root.width/2
Drag.hotSpot.y: root.height/2
Drag.keys: ["x-status-draggable-list-item-internal"]
/*!
\qmlproperty readonly bool StatusDraggableListItem::dragActive
This property holds whether a drag is currently in progress
*/
readonly property bool dragActive: draggable && dragHandler.drag.active
onDragActiveChanged: {
if (dragActive)
Drag.start()
else
Drag.drop()
}
states: [
State {
when: root.dragActive
ParentChange {
target: root
parent: root.dragParent
}
AnchorChanges {
target: root
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
}
}
]
background: Rectangle {
color: "transparent"
border.width: 1
border.color: Theme.palette.baseColor2
radius: 8
MouseArea {
id: dragHandler
anchors.fill: parent
drag.target: root
drag.axis: Drag.YAxis
preventStealing: true // otherwise DND is broken inside a Flickable/ScrollView
hoverEnabled: true
cursorShape: root.dragActive ? Qt.ClosedHandCursor : Qt.PointingHandCursor
}
}
// inset to simulate spacing
topInset: 6
bottomInset: 6
horizontalPadding: 12
verticalPadding: 16
spacing: 8
icon.width: 20
icon.height: 20
contentItem: RowLayout {
spacing: root.spacing
StatusIcon {
icon: "justify"
visible: root.draggable
}
StatusIcon {
Layout.preferredWidth: root.icon.width
Layout.preferredHeight: root.icon.height
Layout.leftMargin: root.spacing/2
icon: root.icon.name
color: root.icon.color
}
StatusBaseText {
Layout.fillWidth: true
text: root.title
elide: Text.ElideRight
maximumLineCount: 1
}
// TODO secondaryTitle
RowLayout {
id: actionsRow
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
spacing: 12
}
}
}

View File

@ -16,6 +16,7 @@ StatusContactVerificationIcons 0.1 StatusContactVerificationIcons.qml
StatusDateGroupLabel 0.1 StatusDateGroupLabel.qml
StatusDescriptionListItem 0.1 StatusDescriptionListItem.qml
StatusDotsLoadingIndicator 0.1 StatusDotsLoadingIndicator.qml
StatusDraggableListItem 0.1 StatusDraggableListItem.qml
StatusLetterIdenticon 0.1 StatusLetterIdenticon.qml
StatusListItem 0.1 StatusListItem.qml
StatusListSectionHeadline 0.1 StatusListSectionHeadline.qml

View File

@ -194,11 +194,10 @@ QC.Popup {
parent: QC.Overlay.overlay
width: 480
// implicitHeight: headerImpl.implicitHeight + contentItem.implicitHeight + footerImpl.implicitHeight
padding: 0
topPadding: padding + headerImpl.implicitHeight
bottomPadding: padding + footerImpl.implicitHeight
topPadding: padding + headerImpl.height
bottomPadding: padding + footerImpl.height
leftPadding: padding
rightPadding: padding
@ -218,6 +217,7 @@ QC.Popup {
id: headerImpl
anchors.top: parent.top
width: visible ? parent.width : 0
height: visible ? implicitHeight : 0
visible: root.showHeader
title: header.title
@ -246,6 +246,7 @@ QC.Popup {
id: footerImpl
anchors.bottom: parent.bottom
width: visible ? parent.width : 0
height: visible ? implicitHeight : 0
showFooter: root.showFooter
visible: root.showFooter
}

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 13.5977L5 13.5977" stroke="#939BA1" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M15 5.59766L5 5.59766" stroke="#939BA1" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M15 9.59766L5 9.59766" stroke="#939BA1" stroke-width="1.3" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@ -257,7 +257,7 @@ Item {
property var categoryItem
StatusAction {
text: categoryItem.muted ? qsTr("Unmute category") : qsTr("Mute category")
text: !!categoryItem ? categoryItem.muted ? qsTr("Unmute category") : qsTr("Mute category") : ""
icon.name: "notification"
onTriggered: {
if (categoryItem.muted) {

View File

@ -1,101 +0,0 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import utils 1.0
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import shared.controls 1.0
Item {
id: root
property alias hyperlink: hyperlinkInput.text
property alias url: urlInput.text
readonly property alias removeButton: removeButton
readonly property var focusItem: hyperlinkInput.input.edit
property var nextFocusItem: null
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
RowLayout {
id: layout
anchors.fill: parent
ColumnLayout {
id: leftLayout
spacing: 26
StatusInput {
id: hyperlinkInput
objectName: "hyperlinkInput"
Layout.fillWidth: true
label: qsTr("Hyperlink Text")
placeholderText: qsTr("Example: My Myspace Profile")
charLimit: 24
input.tabNavItem: urlInput.input.edit
}
StatusInput {
id: urlInput
objectName: "urlInput"
Layout.fillWidth: true
label: qsTr("URL")
placeholderText: qsTr("Link URL")
input.tabNavItem: root.nextFocusItem
}
}
Item {
Layout.preferredWidth: 280
Layout.fillHeight: true
clip: true
ColumnLayout {
x: 64
anchors.verticalCenter: parent.verticalCenter
spacing: 10
StatusBaseText {
text: qsTr("Preview")
color: Theme.palette.baseColor1
font.pixelSize: 15
}
SocialLinkPreview {
Layout.preferredHeight: 32
text: !!hyperlinkInput.text ? hyperlinkInput.text : qsTr("My Myspace Profile")
url: !!urlInput.text ? urlInput.text : urlInput.placeholderText
linkType: Constants.socialLinkType.custom
}
}
}
}
StatusFlatButton {
id: removeButton
anchors {
right: parent.right
top: parent.top
}
icon.name: "delete"
text: qsTr("Remove")
size: StatusBaseButton.Small
}
}

View File

@ -2,7 +2,6 @@ import QtQuick 2.14
import utils 1.0
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
StatusInput {
@ -11,19 +10,13 @@ StatusInput {
property int linkType
property string icon
leftPadding: 18 // by design
leftPadding: Style.current.padding
input.clearable: true
placeholderText: {
if (linkType === Constants.socialLinkType.twitter) return qsTr("Twitter Handle")
if (linkType === Constants.socialLinkType.personalSite) return qsTr("Personal Site")
if (linkType === Constants.socialLinkType.github) return qsTr("Github")
if (linkType === Constants.socialLinkType.youtube) return qsTr("YouTube Channel")
if (linkType === Constants.socialLinkType.discord) return qsTr("Discord Handle")
if (linkType === Constants.socialLinkType.telegram) return qsTr("Telegram Handle")
return ""
}
placeholderText: ProfileUtils.linkTypeToDescription(linkType)
input.asset {
name: root.icon
color: ProfileUtils.linkTypeColor(root.linkType)
width: 20
height: 20
}

View File

@ -1,2 +1,3 @@
CommunityDelegate 1.0 CommunityDelegate.qml
WalletAccountDelegate 1.0 WalletAccountDelegate.qml
StaticSocialLinkInput 1.0 StaticSocialLinkInput.qml

View File

@ -15,11 +15,6 @@ Item {
property alias displayName: displayNameInput
property alias bio: bioInput
property var socialLinksModel
signal socialLinkChanged(string uuid, string text, string url)
signal addSocialLinksClicked
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
@ -59,39 +54,7 @@ Item {
maximumHeight: 108
input.verticalAlignment: TextEdit.AlignTop
input.tabNavItem: socialLinksRepeater.count ? socialLinksRepeater.itemAt(0).input.edit : null
}
Repeater {
id: socialLinksRepeater
model: root.socialLinksModel
delegate: StaticSocialLinkInput {
objectName: model.text + "-socialLinkInput"
Layout.fillWidth: true
linkType: model.linkType
text: Utils.stripSocialLinkPrefix(model.url, model.linkType)
icon: model.icon
onTextChanged: root.socialLinkChanged(model.uuid, model.text, Utils.addSocialLinkPrefix(text, model.linkType))
input.tabNavItem: {
if (index < socialLinksRepeater.count - 1) {
return socialLinksRepeater.itemAt(index + 1).input.edit
}
return addMoreSocialLinksButton
}
}
}
StatusIconTextButton {
id: addMoreSocialLinksButton
objectName: "addMoreSocialLinksButton"
Layout.topMargin: -8 // by design
text: qsTr("Add more social links")
onClicked: root.addSocialLinksClicked()
input.tabNavItem: displayNameInput.input.edit
}
}
}

View File

@ -0,0 +1,160 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Shapes 1.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
import AppLayouts.Profile.popups 1.0
import SortFilterProxyModel 0.2
Control {
id: root
property var profileStore
property var socialLinksModel
background: null
implicitHeight: layout.implicitHeight + linksView.contentHeight
Component {
id: addSocialLinkModalComponent
AddSocialLinkModal {
onAddLinkRequested: root.profileStore.createLink(linkText, linkUrl, linkType, linkIcon)
}
}
Component {
id: modifySocialLinkModal
ModifySocialLinkModal {
onUpdateLinkRequested: root.profileStore.updateLink(uuid, linkText, linkUrl)
onRemoveLinkRequested: root.profileStore.removeLink(uuid)
}
}
contentItem: ColumnLayout {
id: layout
spacing: 12
RowLayout {
Layout.fillWidth: true
Layout.bottomMargin: Style.current.halfPadding
StatusBaseText {
text: qsTr("Links")
color: Theme.palette.baseColor1
}
Item { Layout.fillWidth: true }
StatusLinkText {
text: qsTr(" Add more links")
color: Theme.palette.primaryColor1
font.pixelSize: Theme.tertiaryTextFontSize
font.weight: Font.Normal
onClicked: Global.openPopup(addSocialLinkModalComponent)
}
}
SortFilterProxyModel {
id: filteredSocialLinksModel
sourceModel: root.socialLinksModel
filters: ExpressionFilter {
expression: model.text !== "" && model.url !== ""
}
}
// empty placeholder when no links; dashed rounded rectangle
ShapeRectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width - 4 // the rectangular path is rendered outside
Layout.preferredHeight: 44
visible: !filteredSocialLinksModel.count
text: qsTr("Your links will appear here")
}
StatusListView {
id: linksView
Layout.fillWidth: true
Layout.fillHeight: true
model: root.socialLinksModel
displaced: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
}
delegate: DropArea {
id: delegateRoot
property int visualIndex: index
width: ListView.view.width
height: draggableDelegate.height
keys: ["x-status-draggable-list-item-internal"]
onEntered: function(drag) {
const from = drag.source.visualIndex
const to = draggableDelegate.visualIndex
if (to === from)
return
root.profileStore.moveLink(from, to, 1)
drag.accept()
}
onDropped: function(drop) {
root.profileStore.saveSocialLinks(true /*silent*/)
}
StatusDraggableListItem {
id: draggableDelegate
readonly property string asideText: ProfileUtils.stripSocialLinkPrefix(model.url, model.linkType)
visible: !!asideText
width: parent.width
height: visible ? implicitHeight : 0
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
dragParent: linksView
visualIndex: delegateRoot.visualIndex
draggable: linksView.count > 1
title: ProfileUtils.linkTypeToText(model.linkType) || model.text
icon.name: model.icon
icon.color: ProfileUtils.linkTypeColor(model.linkType)
actions: [
StatusLinkText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
horizontalAlignment: Text.AlignRight
font.pixelSize: Theme.primaryTextFontSize
font.weight: Font.Normal
text: draggableDelegate.asideText
onClicked: Global.openLink(model.url)
},
StatusFlatRoundButton {
icon.name: "edit_pencil"
Layout.preferredWidth: 24
Layout.preferredHeight: 24
icon.width: 16
icon.height: 16
type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Edit link")
onClicked: Global.openPopup(modifySocialLinkModal,
{linkType: model.linkType, icon: model.icon, uuid: model.uuid,
linkText: model.text, linkUrl: draggableDelegate.asideText})
}
]
}
}
}
}
}

View File

@ -0,0 +1,108 @@
import QtQuick 2.15
import QtQuick.Shapes 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
/*!
\qmltype ShapeRectangle
\inherits Shape
\brief Rectangle-like component with the ability to further customize the outline/border style using a Shape/ShapePath;
with optional text in the middle
Example of how to use it:
\qml
ShapeRectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width - 4 // the rectangular path is rendered outside
Layout.preferredHeight: 44
text: qsTr("Your links will appear here")
}
\endqml
\sa Shape
\sa ShapePath
*/
Shape {
id: root
property string text
readonly property int radius: Style.current.radius
readonly property alias path: path
asynchronous: true
// design values; Shape doesn't have an implicit size
implicitWidth: 448
implicitHeight: 44
StatusBaseText {
anchors.centerIn: parent
color: Theme.palette.baseColor1
text: root.text
}
ShapePath {
id: path
fillColor: "transparent"
strokeColor: Theme.palette.baseColor2
strokeWidth: 1
strokeStyle: ShapePath.DashLine
dashPattern: [4, 4]
startX: root.radius
startY: 0
PathLine {
x: root.width - root.radius
y: 0
}
PathCubic {
control1X: root.width
control2X: root.width
control1Y: 0
control2Y: 0
x: root.width
y: root.radius
}
PathLine {
x: root.width
y: root.height - root.radius
}
PathCubic {
control1X: root.width
control2X: root.width
control1Y: root.height
control2Y: root.height
x: root.width - root.radius
y: root.height
}
PathLine {
x: root.radius
y: root.height
}
PathCubic {
control1X: 0
control2X: 0
control1Y: root.height
control2Y: root.height
x: 0
y: root.height - root.radius
}
PathLine {
x: 0
y: root.radius
}
PathCubic {
control1X: 0
control2X: 0
control1Y: 0
control2Y: 0
x: root.radius
y: 0
}
}
}

View File

@ -0,0 +1 @@
ProfileSocialLinksPanel 1.0 ProfileSocialLinksPanel.qml

View File

@ -0,0 +1,127 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import utils 1.0
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import AppLayouts.Profile.controls 1.0
StatusStackModal {
id: root
signal addLinkRequested(string linkText, string linkUrl, int linkType, string linkIcon)
implicitWidth: 480 // design
implicitHeight: 512 // design
anchors.centerIn: parent
padding: currentIndex === 0 ? 0 : Style.current.padding
header.title: qsTr("Add more links")
rightButtons: [finishButton]
finishButton: StatusButton {
text: qsTr("Add")
enabled: !!linkTarget.text
onClicked: {
root.addLinkRequested(d.selectedLinkTypeText || customTitle.text, // text for custom link, otherwise the link typeId
ProfileUtils.addSocialLinkPrefix(linkTarget.text, d.selectedLinkType),
d.selectedLinkType, d.selectedIcon)
root.close();
}
}
showFooter: currentIndex > 0
onClosed: destroy()
QtObject {
id: d
property int selectedLinkIndex: -1
readonly property int selectedLinkType: d.selectedLinkIndex !== -1 ? staticLinkTypesModel.get(d.selectedLinkIndex).type : 0
readonly property string selectedLinkTypeText: d.selectedLinkIndex !== -1 ? staticLinkTypesModel.get(d.selectedLinkIndex).text : ""
readonly property string selectedIcon: d.selectedLinkIndex !== -1 ? staticLinkTypesModel.get(d.selectedLinkIndex).icon : ""
readonly property var staticLinkTypesModel: ListModel {
readonly property var data: [
{ type: Constants.socialLinkType.twitter, icon: "twitter", text: "__twitter" },
{ type: Constants.socialLinkType.personalSite, icon: "language", text: "__personal_site" },
{ type: Constants.socialLinkType.github, icon: "github", text: "__github" },
{ type: Constants.socialLinkType.youtube, icon: "youtube", text: "__youtube" },
{ type: Constants.socialLinkType.discord, icon: "discord", text: "__discord" },
{ type: Constants.socialLinkType.telegram, icon: "telegram", text: "__telegram" },
{ type: Constants.socialLinkType.custom, icon: "link", text: "" }
]
Component.onCompleted: append(data)
}
}
onCurrentIndexChanged: {
//StatusAnimatedStack doesn't handle well items' visibility,
//keeping this solution for now until #8024 is fixed
if (currentIndex === 1) {
customTitle.input.edit.clear()
linkTarget.input.edit.clear()
if (d.selectedLinkType === Constants.socialLinkType.custom)
customTitle.input.edit.forceActiveFocus()
else
linkTarget.input.edit.forceActiveFocus()
}
}
stackItems: [
StatusListView {
width: root.availableWidth
height: root.availableHeight
model: d.staticLinkTypesModel
delegate: StatusListItem {
width: ListView.view.width
title: ProfileUtils.linkTypeToText(model.type) || qsTr("Custom link")
asset.name: model.icon
asset.color: ProfileUtils.linkTypeColor(model.type)
onClicked: {
d.selectedLinkIndex = index
root.currentIndex++
}
components: [
StatusIcon {
anchors.verticalCenter: parent.verticalCenter
icon: "next"
color: Theme.palette.baseColor1
}
]
}
},
ColumnLayout {
width: root.availableWidth
spacing: Style.current.halfPadding
StaticSocialLinkInput {
id: customTitle
Layout.fillWidth: true
visible: d.selectedLinkType === Constants.socialLinkType.custom
placeholderText: ""
label: qsTr("Add a title")
linkType: Constants.socialLinkType.custom
icon: "language"
charLimit: Constants.maxSocialLinkTextLength
input.tabNavItem: linkTarget.input.edit
}
StaticSocialLinkInput {
id: linkTarget
Layout.fillWidth: true
Layout.topMargin: customTitle.visible ? Style.current.padding : 0
placeholderText: ""
label: ProfileUtils.linkTypeToDescription(linkType) || qsTr("Add your link")
linkType: d.selectedLinkType
icon: d.selectedIcon
input.tabNavItem: customTitle.input.edit
}
}
]
}

View File

@ -0,0 +1,99 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import utils 1.0
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups.Dialog 0.1
import AppLayouts.Profile.controls 1.0
StatusDialog {
id: root
property int linkType: -1
property string icon
property string uuid
property string linkText
property string linkUrl
signal updateLinkRequested(string uuid, string linkText, string linkUrl)
signal removeLinkRequested(string uuid)
implicitWidth: 480 // design
title: ProfileUtils.linkTypeToText(linkType) || qsTr("Modify Custom Link")
footer: StatusDialogFooter {
leftButtons: ObjectModel {
StatusButton {
type: StatusButton.Danger
text: qsTr("Delete")
onClicked: {
root.removeLinkRequested(root.uuid)
root.close()
}
}
}
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Update")
enabled: {
if (!customTitle.visible)
return linkTarget.text && linkTarget.text !== linkUrl
return linkTarget.text && (linkTarget.text !== linkUrl || (customTitle.text && customTitle.text !== root.linkText))
}
onClicked: {
root.updateLinkRequested(root.uuid, customTitle.text, ProfileUtils.addSocialLinkPrefix(linkTarget.text, root.linkType))
root.close()
}
}
}
}
onAboutToShow: {
if (linkType === Constants.socialLinkType.custom)
customTitle.input.edit.forceActiveFocus()
else
linkTarget.input.edit.forceActiveFocus()
}
onClosed: destroy()
contentItem: ColumnLayout {
width: root.availableWidth
spacing: Style.current.halfPadding
StaticSocialLinkInput {
id: customTitle
Layout.fillWidth: true
visible: root.linkType === Constants.socialLinkType.custom
placeholderText: ""
label: qsTr("Change title")
linkType: Constants.socialLinkType.custom
icon: "language"
text: root.linkText
charLimit: Constants.maxSocialLinkTextLength
input.tabNavItem: linkTarget.input.edit
}
StaticSocialLinkInput {
id: linkTarget
Layout.fillWidth: true
Layout.topMargin: customTitle.visible ? Style.current.padding : 0
placeholderText: ""
label: ProfileUtils.linkTypeToDescription(linkType) || qsTr("Modify your link")
linkType: root.linkType
icon: root.icon
text: root.linkUrl
input.tabNavItem: customTitle.input.edit
}
}
}

View File

@ -1,160 +0,0 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import utils 1.0
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Popups.Dialog 0.1
import "../stores"
import "../controls"
import SortFilterProxyModel 0.2
StatusDialog {
id: root
objectName: "socialLinksModal"
property ProfileStore profileStore
width: 640
topPadding: 24
bottomPadding: 24
leftPadding: 34
rightPadding: 34
title: qsTr("Social Links")
footer: null
onOpened: {
staticLinksRepeater.model = staticSocialLinks
customLinksRepeater.model = customSocialLinks
}
onClosed: {
// ensure text input values are reevaluated
staticLinksRepeater.model = null
customLinksRepeater.model = null
}
SortFilterProxyModel {
id: staticSocialLinks
sourceModel: root.profileStore.temporarySocialLinksModel
filters: ValueFilter {
roleName: "linkType"
value: Constants.socialLinkType.custom
inverted: true
}
sorters: RoleSorter {
roleName: "linkType"
}
}
SortFilterProxyModel {
id: customSocialLinks
sourceModel: root.profileStore.temporarySocialLinksModel
filters: ValueFilter {
roleName: "linkType"
value: Constants.socialLinkType.custom
}
}
StatusScrollView {
id: scrollView
implicitHeight: contentHeight + topPadding + bottomPadding
anchors.fill: parent
padding: 0
ColumnLayout {
width: scrollView.availableWidth
spacing: 24 // by design
Repeater {
id: staticLinksRepeater
delegate: StaticSocialLinkInput {
objectName: model.text + "-socialLinkInput"
Layout.fillWidth: true
linkType: model.linkType
text: Utils.stripSocialLinkPrefix(model.url, model.linkType)
icon: model.icon
input.tabNavItem: {
if (index < staticLinksRepeater.count - 1) {
return staticLinksRepeater.itemAt(index + 1).input.edit
}
return customLinksRepeater.count ? customLinksRepeater.itemAt(0).focusItem : null
}
onTextChanged: root.profileStore.updateLink(model.uuid, model.text, Utils.addSocialLinkPrefix(text, linkType))
Component.onCompleted: if (index === 0) {
input.edit.forceActiveFocus()
}
}
}
StatusBaseText {
text: qsTr("Custom links")
color: Theme.palette.baseColor1
font.pixelSize: 15
}
ColumnLayout {
id: customLinksLayout
spacing: 40
Layout.topMargin: -4 // by design
Repeater {
id: customLinksRepeater
delegate: CustomSocialLinkInput {
objectName: "customSocialLinkInput"
Layout.fillWidth: true
hyperlink: model.text
url: model.url
removeButton.visible: index > 0
removeButton.onClicked: root.profileStore.removeCustomLink(model.uuid)
nextFocusItem: {
if (index < customLinksRepeater.count - 1) {
return customLinksRepeater.itemAt(index + 1).focusItem
}
return addAnotherCustomLinkButton
}
onHyperlinkChanged: root.profileStore.updateLink(model.uuid, hyperlink, url)
onUrlChanged: root.profileStore.updateLink(model.uuid, hyperlink, url)
Rectangle {
y: -customLinksLayout.spacing / 2
width: parent.width
height: 1
color: Theme.palette.baseColor2
visible: index > 0
}
}
}
}
StatusIconTextButton {
id: addAnotherCustomLinkButton
text: qsTr("Add another custom link")
onClicked: {
root.profileStore.createCustomLink("", "")
scrollView.contentY = scrollView.contentHeight - scrollView.availableHeight
}
}
}
}
}

View File

@ -1,2 +1,4 @@
BackupSeedModal 1.0 BackupSeedModal.qml
SetupSyncingPopup 1.0 SetupSyncingPopup.qml
AddSocialLinkModal 1.0 AddSocialLinkModal.qml
ModifySocialLinkModal 1.0 ModifySocialLinkModal.qml

View File

@ -22,6 +22,7 @@ QtObject {
readonly property string socialLinksJson: profileModule.socialLinksJson
readonly property var socialLinksModel: profileModule.socialLinksModel
readonly property var temporarySocialLinksModel: profileModule.temporarySocialLinksModel // for editing purposes
readonly property var temporarySocialLinksJson: profileModule.temporarySocialLinksJson
readonly property bool socialLinksDirty: profileModule.socialLinksDirty
onUserDeclinedBackupBannerChanged: {
@ -52,24 +53,28 @@ QtObject {
root.profileModule.setDisplayName(displayName)
}
function createCustomLink(text, url) {
root.profileModule.createCustomLink(text, url)
function createLink(text, url, linkType, icon) {
root.profileModule.createLink(text, url, linkType, icon)
}
function removeCustomLink(uuid) {
root.profileModule.removeCustomLink(uuid)
function removeLink(uuid) {
root.profileModule.removeLink(uuid)
}
function updateLink(uuid, text, url) {
root.profileModule.updateLink(uuid, text, url)
}
function moveLink(fromRow, toRow, count) {
root.profileModule.moveLink(fromRow, toRow)
}
function resetSocialLinks() {
root.profileModule.resetSocialLinks()
}
function saveSocialLinks() {
root.profileModule.saveSocialLinks()
function saveSocialLinks(silent = false) {
root.profileModule.saveSocialLinks(silent)
}
function setBio(bio) {

View File

@ -18,11 +18,11 @@ import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import SortFilterProxyModel 0.2
ColumnLayout {
id: root
spacing: 20
property PrivacyStore privacyStore
property ProfileStore profileStore
property WalletStore walletStore
@ -48,7 +48,6 @@ ColumnLayout {
descriptionPanel.displayName.text = Qt.binding(() => { return profileStore.displayName })
descriptionPanel.bio.text = Qt.binding(() => { return profileStore.bio })
profileStore.resetSocialLinks()
descriptionPanel.reevaluateSocialLinkInputs()
biometricsSwitch.checked = Qt.binding(() => { return biometricsSwitch.currentStoredValue })
profileHeader.icon = Qt.binding(() => { return profileStore.profileLargeImage })
}
@ -93,8 +92,6 @@ ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
Layout.topMargin: 20
Layout.bottomMargin: 20
store: root.profileStore
@ -109,33 +106,11 @@ ColumnLayout {
editImageButtonVisible: true
}
SortFilterProxyModel {
id: staticSocialLinksSubsetModel
function filterPredicate(linkType) {
return linkType === Constants.socialLinkType.twitter ||
linkType === Constants.socialLinkType.personalSite
}
sourceModel: profileStore.temporarySocialLinksModel
filters: ExpressionFilter {
expression: staticSocialLinksSubsetModel.filterPredicate(model.linkType)
}
sorters: RoleSorter {
roleName: "linkType"
}
}
ProfileDescriptionPanel {
id: descriptionPanel
readonly property bool isEnsName: profileStore.preferredName
function reevaluateSocialLinkInputs() {
socialLinksModel = null
socialLinksModel = staticSocialLinksSubsetModel
}
Layout.fillWidth: true
displayName.focus: !isEnsName
@ -144,17 +119,22 @@ ColumnLayout {
displayName.validationMode: StatusInput.ValidationMode.Always
displayName.validators: isEnsName ? [] : Constants.validators.displayName
bio.text: profileStore.bio
socialLinksModel: staticSocialLinksSubsetModel
onSocialLinkChanged: profileStore.updateLink(uuid, text, url)
onAddSocialLinksClicked: socialLinksModal.open()
}
SocialLinksModal {
id: socialLinksModal
profileStore: root.profileStore
Separator {
Layout.fillWidth: true
}
onClosed: descriptionPanel.reevaluateSocialLinkInputs()
ProfileSocialLinksPanel {
id: socialLinksPanel
Layout.fillWidth: true
profileStore: root.profileStore
socialLinksModel: root.profileStore.temporarySocialLinksModel
}
Separator {
Layout.fillWidth: true
}
StatusListItem {
@ -171,6 +151,11 @@ ColumnLayout {
onClicked: biometricsSwitch.toggle()
}
Separator {
Layout.fillWidth: true
visible: Qt.platform.os === Constants.mac
}
StatusTabBar {
id: showcaseTabBar
Layout.fillWidth: true

View File

@ -23,16 +23,6 @@ Control {
QtObject {
id: d
function textToType(text) {
if (text === "__twitter") return Constants.socialLinkType.twitter
if (text === "__personal_site") return Constants.socialLinkType.personalSite
if (text === "__github") return Constants.socialLinkType.github
if (text === "__youtube") return Constants.socialLinkType.youtube
if (text === "__discord") return Constants.socialLinkType.discord
if (text === "__telegram") return Constants.socialLinkType.telegram
return Constants.socialLinkType.custom
}
// Unfortunately, nim can't expose temporary QObjects thorugh slots
// The only way to expose static models on demand is through json strings (see getContactDetailsAsJson)
// Model is built here manually, which I know is completely wrong..
@ -46,9 +36,9 @@ Control {
for (let i=0; i<links.length; i++) {
let obj = links[i]
const url = obj.url
const type = textToType(obj.text)
const type = ProfileUtils.linkTextToType(obj.text)
socialLinksModel.append({
"text": Utils.stripSocialLinkPrefix(url, type),
"text": type === Constants.socialLinkType.custom ? obj.text : ProfileUtils.stripSocialLinkPrefix(url, type),
"url": url,
"linkType": type,
"icon": obj.icon
@ -68,21 +58,10 @@ Control {
SortFilterProxyModel {
id: sortedSocialLinksModel
function customsLastPredicate(linkTypeLeft, linkTypeRight) {
if (linkTypeLeft === Constants.socialLinkType.custom) return false
if (linkTypeRight === Constants.socialLinkType.custom) return true
return linkTypeLeft < linkTypeRight
}
sourceModel: socialLinksModel
filters: ExpressionFilter {
expression: model.text !== "" && model.url !== ""
}
sorters: [
ExpressionSorter {
expression: sortedSocialLinksModel.customsLastPredicate(modelLeft.linkType, modelRight.linkType)
}
]
}
contentItem: ColumnLayout {

View File

@ -585,8 +585,8 @@ Pane {
Layout.leftMargin: column.anchors.leftMargin + Style.current.halfPadding
Layout.rightMargin: column.anchors.rightMargin + Style.current.halfPadding
bio: root.dirty ? root.dirtyValues.bio : d.contactDetails.bio
userSocialLinksJson: root.dirty ? root.profileStore.temporarySocialLinksJson
: d.contactDetails.socialLinks
userSocialLinksJson: root.readOnly ? root.profileStore.temporarySocialLinksJson
: d.contactDetails.socialLinks
}
GridLayout {
@ -610,7 +610,7 @@ Pane {
StatusBaseInput {
Layout.fillWidth: true
Layout.preferredHeight: 56
leftPadding: 14
leftPadding: Style.current.padding
rightPadding: Style.current.halfPadding
topPadding: 0
bottomPadding: 0
@ -644,7 +644,7 @@ Pane {
StatusBaseInput {
Layout.fillWidth: true
Layout.preferredHeight: 56
leftPadding: 14
leftPadding: Style.current.padding
rightPadding: Style.current.halfPadding
topPadding: 0
bottomPadding: 0

View File

@ -577,6 +577,14 @@ QtObject {
}
}
readonly property QtObject regularExpressions: QtObject {
readonly property var alphanumericalExpanded: /^$|^[a-zA-Z0-9\-_ ]+$/
}
readonly property QtObject errorMessages: QtObject {
readonly property string alphanumericalExpandedRegExp: qsTr("Only letters, numbers, underscores, whitespaces and hyphens allowed")
}
readonly property QtObject socialLinkType: QtObject {
readonly property int custom: 0
readonly property int twitter: 1
@ -587,6 +595,8 @@ QtObject {
readonly property int telegram: 6
}
readonly property int maxSocialLinkTextLength: 24
readonly property QtObject localPairingEventType: QtObject {
readonly property int eventUnknown: -1
readonly property int eventConnectionError: 0
@ -613,14 +623,6 @@ QtObject {
Finished = 4
}
readonly property QtObject regularExpressions: QtObject {
readonly property var alphanumericalExpanded: /^$|^[a-zA-Z0-9\-_ ]+$/
}
readonly property QtObject errorMessages: QtObject {
readonly property string alphanumericalExpandedRegExp: qsTr("Only letters, numbers, underscores, whitespaces and hyphens allowed")
}
readonly property var socialLinkPrefixesByType: [ // NB order must match the "socialLinkType" enum above
"",
"https://twitter.com/",

View File

@ -2,9 +2,62 @@ pragma Singleton
import QtQml 2.14
import StatusQ.Core.Theme 0.1
QtObject {
function displayName(nickName, ensName, displayName, aliasName)
{
return nickName || ensName || displayName || aliasName
}
// social links utils
function addSocialLinkPrefix(link, type) {
const prefix = Constants.socialLinkPrefixesByType[type]
if (link.startsWith(prefix))
return link
return prefix + link
}
function stripSocialLinkPrefix(link, type) {
return link.replace(Constants.socialLinkPrefixesByType[type], "")
}
function linkTypeToText(linkType) {
if (linkType === Constants.socialLinkType.twitter) return qsTr("Twitter")
if (linkType === Constants.socialLinkType.personalSite) return qsTr("Personal Site")
if (linkType === Constants.socialLinkType.github) return qsTr("Github")
if (linkType === Constants.socialLinkType.youtube) return qsTr("YouTube")
if (linkType === Constants.socialLinkType.discord) return qsTr("Discord")
if (linkType === Constants.socialLinkType.telegram) return qsTr("Telegram")
return "" // "custom" link type allows for user defined text
}
function linkTypeColor(linkType) {
if (linkType === Constants.socialLinkType.twitter) return "#03A9F4"
if (linkType === Constants.socialLinkType.github) return "#000000"
if (linkType === Constants.socialLinkType.youtube) return "#FF3000"
if (linkType === Constants.socialLinkType.discord) return "#7289DA"
if (linkType === Constants.socialLinkType.telegram) return "#0088CC"
return Theme.palette.primaryColor1
}
function linkTypeToDescription(linkType) {
if (linkType === Constants.socialLinkType.twitter) return qsTr("Twitter Handle")
if (linkType === Constants.socialLinkType.personalSite) return qsTr("Personal Site")
if (linkType === Constants.socialLinkType.github) return qsTr("Github")
if (linkType === Constants.socialLinkType.youtube) return qsTr("YouTube Channel")
if (linkType === Constants.socialLinkType.discord) return qsTr("Discord Handle")
if (linkType === Constants.socialLinkType.telegram) return qsTr("Telegram Handle")
return ""
}
function linkTextToType(text) {
if (text === "__twitter") return Constants.socialLinkType.twitter
if (text === "__personal_site") return Constants.socialLinkType.personalSite
if (text === "__github") return Constants.socialLinkType.github
if (text === "__youtube") return Constants.socialLinkType.youtube
if (text === "__discord") return Constants.socialLinkType.discord
if (text === "__telegram") return Constants.socialLinkType.telegram
return Constants.socialLinkType.custom
}
}

View File

@ -572,17 +572,6 @@ QtObject {
return JSON.stringify({imagePath: String(imgPath).replace("file://", ""), cropRect: cropRect})
}
function addSocialLinkPrefix(link, type) {
const prefix = Constants.socialLinkPrefixesByType[type]
if (link.startsWith(prefix))
return link
return prefix + link
}
function stripSocialLinkPrefix(link, type) {
return link.replace(Constants.socialLinkPrefixesByType[type], "")
}
// handle translations for section names coming from app_sections_config.nim
function translatedSectionName(sectionType, fallback) {
switch(sectionType) {

View File

@ -666,6 +666,10 @@ DOS_API void DOS_CALL dos_qabstractitemmodel_beginRemoveRows(DosQAbstractItemMod
/// \param vptr The QAbstractItemModel
DOS_API void DOS_CALL dos_qabstractitemmodel_endRemoveRows(DosQAbstractItemModel *vptr);
DOS_API void DOS_CALL dos_qabstractitemmodel_beginMoveRows(DosQAbstractItemModel *vptr, DosQModelIndex *sourceParent, int sourceFirst, int sourceLast,
DosQModelIndex *destinationParent, int destinationChild);
DOS_API void DOS_CALL dos_qabstractitemmodel_endMoveRows(DosQAbstractItemModel *vptr);
/// \brief Calls the QAbstractItemModel::beginInsertColumns() function
/// \param vptr The QAbstractItemModel
/// \param parent The parent QModelIndex

View File

@ -61,6 +61,13 @@ public:
/// @see QAbstractItemModel::endRemoveRows
virtual void publicEndRemoveRows() = 0;
/// @see QAbstractItemModel::beginMoveRows
virtual void publicBeginMoveRows(const QModelIndex &sourceParent, int sourceFirst, int sourceLast,
const QModelIndex &destinationParent, int destinationChild) = 0;
/// @see QAbstractItemModel::endMoveRows
virtual void publicEndMoveRows() = 0;
/// @see QAbstractItemModel::beginInsertColumns
virtual void publicBeginInsertColumns(const QModelIndex &index, int first, int last) = 0;

View File

@ -92,6 +92,13 @@ public:
/// Expose endInsertRows
void publicEndRemoveRows() override;
/// Expose beginMoveRows
void publicBeginMoveRows(const QModelIndex &sourceParent, int sourceFirst, int sourceLast,
const QModelIndex &destinationParent, int destinationChild) override;
/// Expose endMoveRows
void publicEndMoveRows() override;
/// Expose beginInsertColumns
void publicBeginInsertColumns(const QModelIndex &index, int first, int last) override;

View File

@ -1219,6 +1219,23 @@ void dos_qabstractitemmodel_endRemoveRows(::DosQAbstractItemModel *vptr)
model->publicEndRemoveRows();
}
void dos_qabstractitemmodel_beginMoveRows(DosQAbstractItemModel* vptr, ::DosQModelIndex* sourceParent, int sourceFirst, int sourceLast,
DosQModelIndex* destinationParent, int destinationChild)
{
auto object = static_cast<QObject *>(vptr);
auto model = dynamic_cast<DOS::DosIQAbstractItemModelImpl *>(object);
auto sourceIndex = static_cast<QModelIndex *>(sourceParent);
auto destIndex = static_cast<QModelIndex *>(destinationParent);
model->publicBeginMoveRows(*sourceIndex, sourceFirst, sourceLast, *destIndex, destinationChild);
}
void dos_qabstractitemmodel_endMoveRows(DosQAbstractItemModel* vptr)
{
auto object = static_cast<QObject *>(vptr);
auto model = dynamic_cast<DOS::DosIQAbstractItemModelImpl *>(object);
model->publicEndMoveRows();
}
void dos_qabstractitemmodel_beginInsertColumns(::DosQAbstractItemModel *vptr, ::DosQModelIndex *parentIndex, int first, int last)
{
auto object = static_cast<QObject *>(vptr);

View File

@ -190,6 +190,18 @@ void DosQAbstractGenericModel<T>::publicEndRemoveRows()
T::endRemoveRows();
}
template<class T>
void DosQAbstractGenericModel<T>::publicBeginMoveRows(const QModelIndex& sourceParent, int sourceFirst, int sourceLast, const QModelIndex& destinationParent, int destinationChild)
{
T::beginMoveRows(sourceParent, sourceFirst, sourceLast, destinationParent, destinationChild);
}
template<class T>
void DosQAbstractGenericModel<T>::publicEndMoveRows()
{
T::endMoveRows();
}
template<class T>
void DosQAbstractGenericModel<T>::publicBeginResetModel()
{

2
vendor/nimqml vendored

@ -1 +1 @@
Subproject commit e8d27eee2730c1e33913d46078f0e8aa6520b434
Subproject commit 103a186576660d1d1b5ff7f87ba2b83e908b03a1

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit bac7eb08ca233a4ab9177ac16fa4fe0c2b68f79e
Subproject commit 0eef19f80e1980f737894b7e258e2853c023a35a