feat(TransferOwnershipToasts): Created new `ToastsManager.qml` class and new nim backend for special toasts visualization

This is a first step to globalize how toasts are treated in the qml layer:
- Created `ToastsManager.qml` class to deal with all app toasts generation.
- Started moving community transfer ownership related toasts to the new manager class.
- Some small cleanup in `AppMain.qml`

Nim backend:
- Created new api method to deal with extended / action toasts.
- Updated needed model / item with new needed roles.

Closes of #12175
This commit is contained in:
Noelia 2023-11-03 16:55:04 +01:00 committed by Noelia
parent b0e2651ad8
commit 8bede5e569
12 changed files with 294 additions and 63 deletions

View File

@ -14,9 +14,12 @@ type
durationInMs: int
subTitle: string
icon: string
iconColor: string
loading: bool
ephNotifType: EphemeralNotificationType
url: string
actionType: int
actionData: string
details: NotificationDetails
proc initItem*(id: int64,
@ -24,9 +27,12 @@ proc initItem*(id: int64,
durationInMs = 0,
subTitle = "",
icon = "",
iconColor = "",
loading = false,
ephNotifType = EphemeralNotificationType.Default,
url = "",
actionType = 0, # It means, no action enabled
actionData = "",
details: NotificationDetails): Item =
result = Item()
result.id = id
@ -35,9 +41,12 @@ proc initItem*(id: int64,
result.title = title
result.subTitle = subTitle
result.icon = icon
result.iconColor = iconColor
result.loading = loading
result.ephNotifType = ephNotifType
result.url = url
result.actionType = actionType
result.actionData = actionData
result.details = details
proc id*(self: Item): int64 =
@ -58,6 +67,9 @@ proc subTitle*(self: Item): string =
proc icon*(self: Item): string =
self.icon
proc iconColor*(self: Item): string =
self.iconColor
proc loading*(self: Item): bool =
self.loading
@ -67,5 +79,11 @@ proc ephNotifType*(self: Item): EphemeralNotificationType =
proc url*(self: Item): string =
self.url
proc actionType*(self: Item): int =
self.actionType
proc actionData*(self: Item): string =
self.actionData
proc details*(self: Item): NotificationDetails =
self.details

View File

@ -9,9 +9,12 @@ type
Title
SubTitle
Icon
IconColor
Loading
EphNotifType
Url
ActionType
ActionData
QtObject:
type Model* = ref object of QAbstractListModel
@ -39,9 +42,12 @@ QtObject:
ModelRole.Title.int:"title",
ModelRole.SubTitle.int:"subTitle",
ModelRole.Icon.int:"icon",
ModelRole.IconColor.int:"iconColor",
ModelRole.Loading.int:"loading",
ModelRole.EphNotifType.int:"ephNotifType",
ModelRole.Url.int:"url"
ModelRole.Url.int:"url",
ModelRole.ActionType.int:"actionType",
ModelRole.ActionData.int:"actionData"
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -65,12 +71,18 @@ QtObject:
result = newQVariant(item.subTitle)
of ModelRole.Icon:
result = newQVariant(item.icon)
of ModelRole.IconColor:
result = newQVariant(item.iconColor)
of ModelRole.Loading:
result = newQVariant(item.loading)
of ModelRole.EphNotifType:
result = newQVariant(item.ephNotifType.int)
of ModelRole.Url:
result = newQVariant(item.url)
of ModelRole.ActionType:
result = newQVariant(item.actionType)
of ModelRole.ActionData:
result = newQVariant(item.actionData)
proc findIndexById(self: Model, id: int64): int =
for i in 0 ..< self.items.len:

View File

@ -188,6 +188,10 @@ method displayEphemeralNotification*(self: AccessInterface, title: string, subTi
ephNotifType: int, url: string, details = NotificationDetails()) {.base.} =
raise newException(ValueError, "No implementation available")
method displayEphemeralWithActionNotification*(self: AccessInterface, title: string, subTitle: string, icon: string, iconColor: string, loading: bool,
ephNotifType: int, actionType: int, actionData: string, details = NotificationDetails()) {.base.} =
raise newException(ValueError, "No implementation available")
method removeEphemeralNotification*(self: AccessInterface, id: int64) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1274,9 +1274,25 @@ method displayEphemeralNotification*[T](self: Module[T], title: string, subTitle
finalEphNotifType = EphemeralNotificationType.Success
elif(ephNotifType == EphemeralNotificationType.Danger.int):
finalEphNotifType = EphemeralNotificationType.Danger
let item = ephemeral_notification_item.initItem(id, title, TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS, subTitle, icon, "",
loading, finalEphNotifType, url, 0, "", details)
self.view.ephemeralNotificationModel().addItem(item)
let item = ephemeral_notification_item.initItem(id, title, TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS, subTitle, icon,
loading, finalEphNotifType, url, details)
# TO UNIFY with the one above.
# Further refactor will be done in a next step
method displayEphemeralWithActionNotification*[T](self: Module[T], title: string, subTitle: string, icon: string, iconColor: string, loading: bool,
ephNotifType: int, actionType: int, actionData: string, details = NotificationDetails()) =
let now = getTime()
let id = now.toUnix * 1000000000 + now.nanosecond
var finalEphNotifType = EphemeralNotificationType.Default
if(ephNotifType == EphemeralNotificationType.Success.int):
finalEphNotifType = EphemeralNotificationType.Success
elif(ephNotifType == EphemeralNotificationType.Danger.int):
finalEphNotifType = EphemeralNotificationType.Danger
let item = ephemeral_notification_item.initItem(id, title, TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS, subTitle, icon, iconColor,
loading, finalEphNotifType, "", actionType, actionData, details)
self.view.ephemeralNotificationModel().addItem(item)
method displayEphemeralNotification*[T](self: Module[T], title: string, subTitle: string, details: NotificationDetails) =

View File

@ -105,6 +105,12 @@ QtObject:
ephNotifType: int, url: string) {.slot.} =
self.delegate.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url)
# TO UNIFY with the one above. Now creating a specific method for not introuducing regression.
# Further refactor will be done in a next step
proc displayEphemeralWithActionNotification*(self: View, title: string, subTitle: string, icon: string, iconColor: string, loading: bool,
ephNotifType: int, actionType: int, actionData: string) {.slot.} =
self.delegate.displayEphemeralWithActionNotification(title, subTitle, icon, iconColor, loading, ephNotifType, actionType, actionData)
proc removeEphemeralNotification*(self: View, id: string) {.slot.} =
self.delegate.removeEphemeralNotification(id.parseInt)

View File

@ -101,6 +101,18 @@ Control {
Danger
}
/*!
\qmlproperty bool StatusToastMessage::actionRequired
This property holds if the specific toast message will enable a specific UI action apart from an external link navigation.
*/
property bool actionRequired: false
/*!
\qmlproperty bool StatusToastMessage::iconColor
This property holds icon color if it needs a customized color, otherwise, it will depend on toast type.
*/
property string iconColor: ""
/*!
\qmlmethod
This function is used to open the ToastMessage setting all its properties.
@ -150,13 +162,35 @@ Control {
readonly property string openedState: "opened"
readonly property string closedState: "closed"
readonly property string iconColor: switch(root.type) {
readonly property string iconColor: {
// If specified:
if(root.iconColor != "")
return root.iconColor
// If not specified
switch(root.type) {
case StatusToastMessage.Type.Success:
return Theme.palette.successColor1
case StatusToastMessage.Type.Danger:
return Theme.palette.dangerColor1
default:
return Theme.palette.primaryColor1
}
}
readonly property string iconBgColor: {
// If specified:
if(root.iconColor != "")
return Theme.palette.getColor(root.iconColor, 0.1)
// If not specified
switch(root.type) {
case StatusToastMessage.Type.Success:
return Theme.palette.successColor2
case StatusToastMessage.Type.Danger:
return Theme.palette.dangerColor3
default:
return Theme.palette.primaryColor3
}
}
}
@ -240,16 +274,7 @@ Control {
implicitHeight: 32
Layout.alignment: Qt.AlignVCenter
radius: (root.width/2)
color: {
switch(root.type) {
case StatusToastMessage.Type.Success:
return Theme.palette.successColor2
case StatusToastMessage.Type.Danger:
return Theme.palette.dangerColor3
default:
return Theme.palette.primaryColor3
}
}
color: d.iconBgColor
visible: loader.sourceComponent != undefined
Loader {
id: loader
@ -295,7 +320,7 @@ Control {
}
StatusBaseText {
Layout.fillWidth: true
visible: (!root.linkUrl && !!root.secondaryText)
visible: (!linkText.visible && !!root.secondaryText)
height: visible ? contentHeight : 0
font.pixelSize: 13
color: Theme.palette.baseColor1
@ -305,8 +330,10 @@ Control {
maximumLineCount: 2
}
StatusSelectableText {
id: linkText
Layout.fillWidth: true
visible: (!!root.linkUrl)
visible: (!!root.linkUrl) || root.actionRequired
height: visible ? implicitHeight : 0
font.pixelSize: 13
hoveredLinkColor: Theme.palette.primaryColor1

View File

@ -801,36 +801,6 @@ StatusSectionLayout {
Global.displayToastMessage(title1, url, "", true, type, url)
Global.displayToastMessage(title2, url, "", true, type, url)
}
function onSetSignerStateChanged(communityId, communityName, status, url) {
if (root.community.id !== communityId)
return
if (status === Constants.ContractTransactionStatus.Completed) {
Global.displayToastMessage(qsTr("%1 smart contract amended").arg(communityName),
qsTr("View on etherscan"), "", false,
Constants.ephemeralNotificationType.success, url)
Global.displayToastMessage(qsTr("Your device is now the control node for %1. You now have full Community admin rights.").arg(communityName),
qsTr("%1 Community admin"), "", false,
Constants.ephemeralNotificationType.success, "" /*TODO internal link*/)
} else if (status === Constants.ContractTransactionStatus.Failed) {
Global.displayToastMessage(qsTr("%1 smart contract update failed").arg(communityName),
qsTr("View on etherscan"), "", false,
Constants.ephemeralNotificationType.normal, url)
} else if (status === Constants.ContractTransactionStatus.InProgress) {
Global.displayToastMessage(qsTr("Updating %1 smart contract").arg(communityName),
qsTr("View on etherscan"), "", true,
Constants.ephemeralNotificationType.normal, url)
}
}
function onOwnershipLost(communityId, communityName) {
if (root.community.id !== communityId)
return
let type = Constants.ephemeralNotificationType.normal
Global.displayToastMessage(qsTr("Your device is no longer the control node for Doodles.
Your ownership and admin rights for %1 have been transferred to the new owner.").arg(communityName), "", "", false, type, "")
}
}
Connections {

View File

@ -70,6 +70,17 @@ Item {
// set from main.qml
property var sysPalette
// Central UI point for managing app toasts:
ToastsManager {
id: toastsManager
rootStore: appMain.rootStore
rootChatStore: appMain.rootChatStore
communityTokensStore: appMain.communityTokensStore
sendModalPopup: sendModal
}
Connections {
target: rootStore.mainModuleInst
@ -265,10 +276,6 @@ Item {
d.openActivityCenterPopup()
}
function onDisplayToastMessage(title: string, subTitle: string, icon: string, loading: bool, ephNotifType: int, url: string) {
appMain.rootStore.mainModuleInst.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url)
}
function onOpenLink(link: string) {
// Qt sometimes inserts random HTML tags; and this will break on invalid URL inside QDesktopServices::openUrl(link)
link = appMain.rootStore.plainText(link)
@ -1594,15 +1601,22 @@ Item {
primaryText: model.title
secondaryText: model.subTitle
icon.name: model.icon
iconColor: model.iconColor
loading: model.loading
type: model.ephNotifType
linkUrl: model.url
actionRequired: model.actionType !== ToastsManager.ActionType.None
duration: model.durationInMs
onClicked: {
appMain.rootStore.mainModuleInst.ephemeralNotificationClicked(model.timestamp)
this.open = false
}
onLinkActivated: {
if(actionRequired) {
toastsManager.doAction(model.actionType, model.actionData)
return
}
if (link.startsWith("#") && link !== "#") { // internal link to section
const sectionArgs = link.substring(1).split("/")
const section = sectionArgs[0]
@ -1612,7 +1626,6 @@ Item {
else
Global.openLink(link)
}
onClose: {
appMain.rootStore.mainModuleInst.removeEphemeralNotification(model.timestamp)
}

View File

@ -904,9 +904,9 @@ QtObject {
FinaliseOwnershipDeclinePopup {
destroyOnClose: true
onDeclineClicked: {
onDeclineClicked: {
close()
root.ownershipDeclined()
root.rootStore.communityTokensStore.ownershipDeclined(communityId, communityName)
}
}
}

View File

@ -0,0 +1,160 @@
import QtQuick 2.15
import utils 1.0
import AppLayouts.stores 1.0
import AppLayouts.Chat.stores 1.0 as ChatStores
import shared.stores 1.0 as SharedStores
// The purpose of this class is to be the central point for generating toasts in the application.
// It will have as input all needed stores.
// In case the file grows considerably, consider creating different toasts managers per topic / context
// and just instantiate them in here.
QtObject {
id: root
// Here there are defined some specific actions needed by a toast.
// They are normally specific navigations or open popup action.
enum ActionType {
None = 0,
NavigateToCommunityAdmin = 1,
OpenFinaliseOwnershipPopup = 2,
OpenSendModalPopup = 3
}
// Stores:
required property RootStore rootStore
required property ChatStores.RootStore rootChatStore
required property SharedStores.CommunityTokensStore communityTokensStore
// Properties:
required property var sendModalPopup
// Utils:
readonly property string viewOptimismExplorerText: qsTr("View on Optimism Explorer")
readonly property string checkmarkCircleAssetName: "checkmark-circle"
readonly property string crownOffAssetName: "crown-off"
// Community Transfer Ownership related toasts:
readonly property Connections _communityTokensStoreConnections: Connections {
target: root.communityTokensStore
// Ownership Receiver:
function onOwnerTokenReceived(communityId, communityName) {
let communityColor = root.rootChatStore.getCommunityDetailsAsJson(communityId).color
Global.displayToastWithActionMessage(qsTr("You received the Owner token for %1. To finalize ownership, make your device the control node.").arg(communityName),
qsTr("Finalise ownership"),
"crown",
communityColor,
false,
Constants.ephemeralNotificationType.normal,
ToastsManager.ActionType.OpenFinaliseOwnershipPopup,
communityId)
}
function onSetSignerStateChanged(communityId, communityName, status, url) {
if (status === Constants.ContractTransactionStatus.Completed) {
Global.displayToastMessage(qsTr("%1 smart contract amended").arg(communityName),
root.viewOptimismExplorerText,
root.checkmarkCircleAssetName,
false,
Constants.ephemeralNotificationType.success,
url)
Global.displayToastWithActionMessage(qsTr("Your device is now the control node for %1. You now have full Community admin rights.").arg(communityName),
qsTr("%1 Community admin").arg(communityName),
root.checkmarkCircleAssetName,
"",
false,
Constants.ephemeralNotificationType.success,
ToastsManager.ActionType.NavigateToCommunityAdmin,
communityId)
} else if (status === Constants.ContractTransactionStatus.Failed) {
Global.displayToastMessage(qsTr("%1 smart contract update failed").arg(communityName),
root.viewOptimismExplorerText,
"warning",
false,
Constants.ephemeralNotificationType.danger,
url)
} else if (status === Constants.ContractTransactionStatus.InProgress) {
Global.displayToastMessage(qsTr("Updating %1 smart contract").arg(communityName),
root.viewOptimismExplorerText,
"",
true,
Constants.ephemeralNotificationType.normal,
url)
}
}
function onCommunityOwnershipDeclined(communityName) {
Global.displayToastWithActionMessage(qsTr("You declined ownership of %1.").arg(communityName),
qsTr("Return owner token to sender"),
root.crownOffAssetName,
"",
false,
Constants.ephemeralNotificationType.danger,
ToastsManager.ActionType.OpenSendModalPopup,
"")
}
// Ownership Sender:
function onSendOwnerTokenStateChanged(tokenName, status, url) {
if (status === Constants.ContractTransactionStatus.InProgress) {
Global.displayToastMessage(qsTr("Sending %1 token").arg(tokenName),
root.viewOptimismExplorerText,
"",
true,
Constants.ephemeralNotificationType.normal, url)
} else if (status === Constants.ContractTransactionStatus.Completed) {
Global.displayToastMessage(qsTr("%1 token sent").arg(tokenName),
root.viewOptimismExplorerText,
root.checkmarkCircleAssetName,
false,
Constants.ephemeralNotificationType.success, url)
}
}
function onOwnershipLost(communityId, communityName) {
Global.displayToastMessage(qsTr("Your device is no longer the control node for %1.
Your ownership and admin rights for %1 have been transferred to the new owner.").arg(communityName),
"",
root.crownOffAssetName,
false,
Constants.ephemeralNotificationType.danger,
"")
}
}
// Connections to global. These will lead the backend integration:
readonly property Connections _globalConnections: Connections {
target: Global
function onDisplayToastMessage(title: string, subTitle: string, icon: string, loading: bool, ephNotifType: int, url: string) {
root.rootStore.mainModuleInst.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url)
}
// TO UNIFY with the one above.
// Further refactor will be done in a next step
function onDisplayToastWithActionMessage(title: string, subTitle: string, icon: string, iconColor: string, loading: bool, ephNotifType: int, actionType: int, actionData: string) {
root.rootStore.mainModuleInst.displayEphemeralWithActionNotification(title, subTitle, icon, iconColor, loading, ephNotifType, actionType, actionData)
}
}
// It will cover all specific actions (different than open external links) that can be done after clicking toast link text
function doAction(actionType, actionData) {
switch(actionType) {
case ToastsManager.ActionType.NavigateToCommunityAdmin:
root.rootChatStore.setActiveCommunity(actionData)
return
case ToastsManager.ActionType.OpenFinaliseOwnershipPopup:
Global.openFinaliseOwnershipPopup(actionData)
return
case ToastsManager.ActionType.OpenSendModalPopup:
root.sendModalPopup.open()
return
default:
console.warn("ToastsManager: Action type is not defined")
return
}
}
}

View File

@ -31,6 +31,9 @@ QtObject {
signal ownerTokenDeploymentStarted(string communityId, string url)
signal setSignerStateChanged(string communityId, string communityName, int status, string url)
signal ownershipLost(string communityId, string communityName)
signal communityOwnershipDeclined(string communityName)
signal sendOwnerTokenStateChanged(string tokenName, int status, string url)
signal ownerTokenReceived(string communityId, string communityName)
// Minting tokens:
function deployCollectible(communityId, collectibleItem)
@ -72,9 +75,10 @@ QtObject {
communityTokensModuleInst.setSigner(communityId, chainId, contractAddress, accountAddress)
}
function ownershipDeclined(communityId) {
function ownershipDeclined(communityId, communityName) {
communityTokensModuleInst.declineOwnership(communityId)
}
root.communityOwnershipDeclined(communityName)
}
readonly property Connections connections: Connections {
target: communityTokensModuleInst
@ -124,13 +128,7 @@ QtObject {
}
function onOwnerTokenReceived(communityId, communityName, chainId, communityAddress) {
// TODO clicking url should execute finalise flow
Global.displayToastMessage(qsTr("You received the Owner token for %1. To finalize ownership, make your device the control node.").arg(communityName),
qsTr("Finalise ownership"),
"",
false,
Constants.ephemeralNotificationType.normal,
"")
root.ownerTokenReceived(communityId, communityName)
}
function onSetSignerStateChanged(communityId, communityName, status, url) {
@ -140,9 +138,15 @@ QtObject {
function onOwnershipLost(communityId, communityName) {
root.ownershipLost(communityId, communityName)
}
// TODO: BACKEND!!!
function onSendOwnerTokenStateChanged(tokenName, status, url) {
console.warn("TODO: Backend missing! On Send owner token!")
root.onSendOwnerTokenStateChanged(tokenName, status, url)
}
}
// Burn:
// Burn:
function computeBurnFee(tokenKey, amount, accountAddress, requestId) {
console.assert(typeof amount === "string")
communityTokensModuleInst.computeBurnFee(tokenKey, amount, accountAddress, requestId)

View File

@ -25,6 +25,7 @@ QtObject {
signal unblockContactRequested(string publicKey, string contactName)
signal displayToastMessage(string title, string subTitle, string icon, bool loading, int ephNotifType, string url)
signal displayToastWithActionMessage(string title, string subTitle, string icon, string iconColor, bool loading, int ephNotifType, int actionType, string data)
signal openPopupRequested(var popupComponent, var params)
signal closePopupRequested()