feat(@desktop/profile): add ephemeral notifications

- ephemeral notifications implemented as toast messages
- old notifications added aligned with new toast messages

Fixes #5629
This commit is contained in:
Sale Djenic 2022-05-05 12:28:54 +02:00 committed by saledjenic
parent 242d980377
commit 70e770103e
16 changed files with 342 additions and 92 deletions

View File

@ -620,6 +620,8 @@ method onContactUnblocked*(self: Module, publicKey: string) =
self.view.chatsModel().blockUnblockItemOrSubItemById(publicKey, blocked=false)
method onContactDetailsUpdated*(self: Module, publicKey: string) =
if(self.controller.isCommunity()):
return
let contactDetails = self.controller.getContactDetails(publicKey)
if (contactDetails.details.isContactRequestReceived() and
not contactDetails.details.isContactRequestSent() and

View File

@ -188,6 +188,10 @@ proc init*(self: Controller) =
var args = ClickedNotificationArgs(e)
self.delegate.osNotificationClicked(args.details)
self.events.on(SIGNAL_DISPLAY_APP_NOTIFICATION) do(e: Args):
var args = NotificationArgs(e)
self.delegate.displayEphemeralNotification(args.title, args.message, args.details)
self.events.on(SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY) do(e: Args):
var args = CommunityRequestArgs(e)
self.delegate.newCommunityMembershipRequestReceived(args.communityRequest)

View File

@ -0,0 +1,57 @@
type
EphemeralNotificationType* {.pure.} = enum
Default = 0
Success
type
Item* = object
id: int64
title: string
durationInMs: int
subTitle: string
icon: string
loading: bool
ephNotifType: EphemeralNotificationType
url: string
proc initItem*(id: int64,
title: string,
durationInMs = 0,
subTitle = "",
icon = "",
loading = false,
ephNotifType = EphemeralNotificationType.Default,
url = ""): Item =
result = Item()
result.id = id
result.durationInMs = durationInMs
result.title = title
result.subTitle = subTitle
result.icon = icon
result.loading = loading
result.ephNotifType = ephNotifType
result.url = url
proc id*(self: Item): int64 =
self.id
proc title*(self: Item): string =
self.title
proc durationInMs*(self: Item): int =
self.durationInMs
proc subTitle*(self: Item): string =
self.subTitle
proc icon*(self: Item): string =
self.icon
proc loading*(self: Item): bool =
self.loading
proc ephNotifType*(self: Item): EphemeralNotificationType =
self.ephNotifType
proc url*(self: Item): string =
self.url

View File

@ -0,0 +1,95 @@
import NimQml, Tables
import ephemeral_notification_item
type
ModelRole {.pure.} = enum
Id = UserRole + 1
DurationInMs
Title
SubTitle
Icon
Loading
EphNotifType
Url
QtObject:
type Model* = ref object of QAbstractListModel
items*: seq[Item]
proc setup(self: Model) =
self.QAbstractListModel.setup
proc delete(self: Model) =
self.items = @[]
self.QAbstractListModel.delete
proc newModel*(): Model =
new(result, delete)
result.setup
method rowCount(self: Model, index: QModelIndex = nil): int =
return self.items.len
method roleNames(self: Model): Table[int, string] =
{
ModelRole.Id.int:"id",
ModelRole.DurationInMs.int:"durationInMs",
ModelRole.Title.int:"title",
ModelRole.SubTitle.int:"subTitle",
ModelRole.Icon.int:"icon",
ModelRole.Loading.int:"loading",
ModelRole.EphNotifType.int:"ephNotifType",
ModelRole.Url.int:"url"
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.items.len:
return
let item = self.items[index.row]
let enumRole = role.ModelRole
case enumRole:
of ModelRole.Id:
result = newQVariant(item.id)
of ModelRole.DurationInMs:
result = newQVariant(item.durationInMs)
of ModelRole.Title:
result = newQVariant(item.title)
of ModelRole.SubTitle:
result = newQVariant(item.subTitle)
of ModelRole.Icon:
result = newQVariant(item.icon)
of ModelRole.Loading:
result = newQVariant(item.loading)
of ModelRole.EphNotifType:
result = newQVariant(item.ephNotifType.int)
of ModelRole.Url:
result = newQVariant(item.url)
proc findIndexById(self: Model, id: int64): int =
for i in 0 ..< self.items.len:
if(self.items[i].id == id):
return i
return -1
proc addItem*(self: Model, item: Item) =
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginInsertRows(parentModelIndex, self.items.len, self.items.len)
self.items.add(item)
self.endInsertRows()
proc removeItemWithId*(self: Model, id: int64) =
let ind = self.findIndexById(id)
if(ind == -1):
return
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginRemoveRows(parentModelIndex, ind, ind)
self.items.delete(ind)
self.endRemoveRows()

View File

@ -128,6 +128,17 @@ method mnemonicBackedUp*(self: AccessInterface) {.base.} =
method osNotificationClicked*(self: AccessInterface, details: NotificationDetails) {.base.} =
raise newException(ValueError, "No implementation available")
method displayEphemeralNotification*(self: AccessInterface, title: string, subTitle: string, details: NotificationDetails)
{.base.} =
raise newException(ValueError, "No implementation available")
method displayEphemeralNotification*(self: AccessInterface, title: string, subTitle: string, icon: string, loading: bool,
ephNotifType: int, url: string) {.base.} =
raise newException(ValueError, "No implementation available")
method removeEphemeralNotification*(self: AccessInterface, id: int64) {.base.} =
raise newException(ValueError, "No implementation available")
method newCommunityMembershipRequestReceived*(self: AccessInterface, membershipRequest: CommunityMembershipRequestDto)
{.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,6 +1,7 @@
import NimQml, tables, json, sugar, sequtils, strformat, marshal
import NimQml, tables, json, sugar, sequtils, strformat, marshal, times
import io_interface, view, controller, chat_search_item, chat_search_model
import ephemeral_notification_item, ephemeral_notification_model
import ./communities/models/[pending_request_item, pending_request_model]
import ../shared_models/[user_item, user_model, section_item, section_model, active_section]
import ../../global/app_sections_config as conf
@ -57,6 +58,7 @@ import ../../core/eventemitter
export io_interface
const COMMUNITY_PERMISSION_ACCESS_ON_REQUEST = 3
const TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS = 5000 # 5 seconds
type
Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface
@ -697,3 +699,30 @@ method newCommunityMembershipRequestReceived*[T](self: Module[T], membershipRequ
method meMentionedCountChanged*[T](self: Module[T], allMentions: int) =
singletonInstance.globalEvents.meMentionedIconBadgeNotification(allMentions)
method displayEphemeralNotification*[T](self: Module[T], title: string, subTitle: string, icon: string, loading: bool,
ephNotifType: int, url: string) =
let now = getTime()
let id = now.toUnix * 1000000000 + now.nanosecond
var finalEphNotifType = EphemeralNotificationType.Default
if(ephNotifType == EphemeralNotificationType.Success.int):
finalEphNotifType = EphemeralNotificationType.Success
let item = ephemeral_notification_item.initItem(id, title, TOAST_MESSAGE_VISIBILITY_DURATION_IN_MS, subTitle, icon,
loading, finalEphNotifType, url)
self.view.ephemeralNotificationModel().addItem(item)
method displayEphemeralNotification*[T](self: Module[T], title: string, subTitle: string, details: NotificationDetails) =
if(details.notificationType == NotificationType.NewMessage or
details.notificationType == NotificationType.NewMessageWithPersonalMention or
details.notificationType == NotificationType.NewMessageWithGlobalMention):
self.displayEphemeralNotification(title, subTitle, "", false, EphemeralNotificationType.Default.int, "")
elif(details.notificationType == NotificationType.NewContactRequest or
details.notificationType == NotificationType.IdentityVerificationRequest):
self.displayEphemeralNotification(title, subTitle, "contact", false, EphemeralNotificationType.Default.int, "")
elif(details.notificationType == NotificationType.AcceptedContactRequest):
self.displayEphemeralNotification(title, subTitle, "checkmark-circle", false, EphemeralNotificationType.Success.int, "")
method removeEphemeralNotification*[T](self: Module[T], id: int64) =
self.view.ephemeralNotificationModel().removeItemWithId(id)

View File

@ -1,9 +1,10 @@
import NimQml
import NimQml, strutils
import ../shared_models/section_model
import ../shared_models/section_item
import ../shared_models/active_section
import io_interface
import chat_search_model
import ephemeral_notification_model
QtObject:
type
@ -15,6 +16,8 @@ QtObject:
activeSectionVariant: QVariant
chatSearchModel: chat_search_model.Model
chatSearchModelVariant: QVariant
ephemeralNotificationModel: ephemeralNotification_model.Model
ephemeralNotificationModelVariant: QVariant
tmpCommunityId: string # shouldn't be used anywhere except in prepareCommunitySectionModuleForCommunityId/getCommunitySectionModule procs
proc activeSectionChanged*(self:View) {.signal.}
@ -26,6 +29,8 @@ QtObject:
self.activeSectionVariant.delete
self.chatSearchModel.delete
self.chatSearchModelVariant.delete
self.ephemeralNotificationModel.delete
self.ephemeralNotificationModelVariant.delete
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
@ -38,6 +43,8 @@ QtObject:
result.activeSectionVariant = newQVariant(result.activeSection)
result.chatSearchModel = chat_search_model.newModel()
result.chatSearchModelVariant = newQVariant(result.chatSearchModel)
result.ephemeralNotificationModel = ephemeralNotification_model.newModel()
result.ephemeralNotificationModelVariant = newQVariant(result.ephemeralNotificationModel)
signalConnect(result.model, "notificationsCountChanged()", result,
"onNotificationsCountChanged()", 2)
@ -66,21 +73,36 @@ QtObject:
proc chatSearchModel*(self: View): chat_search_model.Model =
return self.chatSearchModel
proc chatSearchModelChanged*(self: View) {.signal.}
proc getChatSearchModel(self: View): QVariant {.slot.} =
return self.chatSearchModelVariant
proc rebuildChatSearchModel*(self: View) {.slot.} =
self.delegate.rebuildChatSearchModel()
proc onNotificationsCountChanged*(self: View) {.slot.} =
self.delegate.meMentionedCountChanged(self.model.allMentionsCount())
proc chatSearchModelChanged*(self: View) {.signal.}
proc getChatSearchModel(self: View): QVariant {.slot.} =
return self.chatSearchModelVariant
QtProperty[QVariant] chatSearchModel:
read = getChatSearchModel
notify = chatSearchModelChanged
proc ephemeralNotificationModel*(self: View): ephemeralNotification_model.Model =
return self.ephemeralNotificationModel
proc ephemeralNotificationModelChanged*(self: View) {.signal.}
proc getEphemeralNotificationModel(self: View): QVariant {.slot.} =
return self.ephemeralNotificationModelVariant
QtProperty[QVariant] ephemeralNotificationModel:
read = getEphemeralNotificationModel
notify = ephemeralNotificationModelChanged
proc displayEphemeralNotification*(self: View, title: string, subTitle: string, icon: string, loading: bool,
ephNotifType: int, url: string) {.slot.} =
self.delegate.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url)
proc removeEphemeralNotification*(self: View, id: string) {.slot.} =
self.delegate.removeEphemeralNotification(id.parseInt)
proc openStoreToKeychainPopup*(self: View) {.signal.}
proc offerToStorePassword*(self: View) =

View File

@ -194,13 +194,13 @@ Rectangle {
}
showToastMessage: function(result) {
// TODO: WIP under PR https://github.com/status-im/status-desktop/pull/4274
//% "Transaction pending..."
toastMessage.title = qsTrId("ens-transaction-pending")
toastMessage.source = Style.svg("loading")
toastMessage.iconColor = Style.current.primary
toastMessage.iconRotates = true
toastMessage.link = `${WalletStore.getEtherscanLink()}/${result}`
toastMessage.open()
let url = `${WalletStore.getEtherscanLink()}/${result}`;
Global.displayToastMessage(qsTr("Transaction pending..."),
"",
"",
true,
Constants.ephemeralNotificationType.normal,
url);
}
}

View File

@ -362,43 +362,37 @@ Item {
Connections {
target: root.store.communitiesModuleInst
onImportingCommunityStateChanged: {
if (state !== Constants.communityImported &&
state !== Constants.communityImportingInProgress &&
state !== Constants.communityImportingError)
{
return
}
Global.toastMessage.close()
let title = ""
let loading = false
if (state === Constants.communityImported)
{
//% "Community imported"
Global.toastMessage.title = qsTrId("community-imported")
Global.toastMessage.source = ""
Global.toastMessage.iconRotates = false
Global.toastMessage.dissapearInMs = 4000
title = qsTrId("community-imported")
}
else if (state === Constants.communityImportingInProgress)
{
//% "Importing community is in progress"
Global.toastMessage.title = qsTrId("importing-community-is-in-progress")
Global.toastMessage.source = Style.svg("loading")
Global.toastMessage.iconRotates = true
Global.toastMessage.dissapearInMs = -1
title = qsTrId("importing-community-is-in-progress")
loading = true
}
else if (state === Constants.communityImportingError)
{
Global.toastMessage.title = errorMsg
Global.toastMessage.source = ""
Global.toastMessage.iconRotates = false
Global.toastMessage.dissapearInMs = 4000
title = errorMsg
}
Global.toastMessage.displayCloseButton = false
Global.toastMessage.displayLink = false
Global.toastMessage.iconColor = Style.current.primary
Global.toastMessage.open()
if(title == "")
{
console.error("unknown state while importing community: ", state)
return
}
Global.displayToastMessage(title,
"",
"",
loading,
Constants.ephemeralNotificationType.normal,
"")
}
}

View File

@ -323,43 +323,52 @@ Item {
Connections {
target: ensView.ensUsernamesStore.ensUsernamesModule
onTransactionWasSent: {
//% "Transaction pending..."
Global.toastMessage.title = qsTrId("ens-transaction-pending")
Global.toastMessage.source = Style.svg("loading")
Global.toastMessage.iconColor = Style.current.primary
Global.toastMessage.iconRotates = true
Global.toastMessage.link = `${ensView.ensUsernamesStore.getEtherscanLink()}/${txResult}`
Global.toastMessage.open()
let url = `${ensView.ensUsernamesStore.getEtherscanLink()}/${txResult}`;
Global.displayToastMessage(qsTr("Transaction pending..."),
"",
"",
true,
Constants.ephemeralNotificationType.normal,
url);
}
onTransactionCompleted: {
let title = ""
switch(trxType){
case "RegisterENS":
Global.toastMessage.title = !success ?
//% "ENS Registration failed"
qsTrId("ens-registration-failed")
:
//% "ENS Registration completed"
qsTrId("ens-registration-completed");
break;
case "SetPubKey":
Global.toastMessage.title = !success ?
//% "Updating ENS pubkey failed"
qsTrId("updating-ens-pubkey-failed")
:
//% "Updating ENS pubkey completed"
qsTrId("updating-ens-pubkey-completed");
break;
case "RegisterENS":
title = !success ?
//% "ENS Registration failed"
qsTrId("ens-registration-failed")
:
//% "ENS Registration completed"
qsTrId("ens-registration-completed");
break;
case "SetPubKey":
title = !success ?
//% "Updating ENS pubkey failed"
qsTrId("updating-ens-pubkey-failed")
:
//% "Updating ENS pubkey completed"
qsTrId("updating-ens-pubkey-completed");
break;
default:
console.error("unknown transaction type: ", trxType);
return
}
let icon = "block-icon";
let ephType = Constants.ephemeralNotificationType.normal;
if (success) {
Global.toastMessage.source = Style.svg("check-circle")
Global.toastMessage.iconColor = Style.current.success
} else {
Global.toastMessage.source = Style.svg("block-icon")
Global.toastMessage.iconColor = Style.current.danger
icon = "check-circle";
ephType = Constants.ephemeralNotificationType.success;
}
Global.toastMessage.link = `${ensView.ensUsernamesStore.getEtherscanLink()}/${txHash}`
Global.toastMessage.open()
let url = `${ensView.ensUsernamesStore.getEtherscanLink()}/${txHash}`;
Global.displayToastMessage(qsTr("Transaction pending..."),
"",
icon,
false,
ephType,
url)
}
}
}

View File

@ -97,6 +97,9 @@ Item {
var popup = backupSeedModalComponent.createObject(appMain)
popup.open()
}
onDisplayToastMessage: {
appMain.rootStore.mainModuleInst.displayEphemeralNotification(title, subTitle, icon, loading, ephNotifType, url);
}
}
function changeAppSectionBySectionId(sectionId) {
@ -742,14 +745,6 @@ Item {
}
}
ToastMessage {
id: toastMessage
Component.onCompleted: {
Global.toastMessage = this;
}
}
DropArea {
id: dragTarget
@ -949,6 +944,34 @@ Item {
}
}
ListView {
id: toastArea
anchors.top: parent.top
anchors.topMargin: 60
anchors.right: parent.right
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 60
spacing: 8
verticalLayoutDirection: ListView.BottomToTop
model: appMain.rootStore.mainModuleInst.ephemeralNotificationModel
delegate: StatusToastMessage {
primaryText: model.title
secondaryText: model.subTitle
icon.name: model.icon
loading: model.loading
type: model.ephNotifType
linkUrl: model.url
duration: model.durationInMs
onLinkActivated: {
Qt.openUrlExternally(link);
}
onClose: {
appMain.rootStore.mainModuleInst.removeEphemeralNotification(model.id)
}
}
}
Component.onCompleted: {
Global.appMain = this;
const whitelist = appMain.rootStore.messagingStore.getLinkPreviewWhitelist()

View File

@ -275,13 +275,13 @@ StatusModal {
return sendingError.open()
}
// % "Transaction pending..."
Global.toastMessage.title = qsTrId("ens-transaction-pending")
Global.toastMessage.source = Style.svg("loading")
Global.toastMessage.iconColor = Style.current.primary
Global.toastMessage.iconRotates = true
Global.toastMessage.link = `${popup.store.getEtherscanLink()}/${response.result}`
Global.toastMessage.open()
let url = `${popup.store.getEtherscanLink()}/${response.result}`
Global.displayToastMessage(qsTr("Transaction pending..."),
"",
"",
true,
Constants.ephemeralNotificationType.normal,
url)
popup.close()
} catch (e) {
console.error('Error parsing the response', e)

View File

@ -363,14 +363,14 @@ StatusModal {
if(isARequest)
root.store.acceptRequestTransaction(transactionId, msgId, root.store.getPubkey() + transactionId.substr(2))
//% "Transaction pending..."
Global.toastMessage.title = qsTrId("ens-transaction-pending")
Global.toastMessage.source = Style.svg("loading")
Global.toastMessage.iconColor = Style.current.primary
Global.toastMessage.iconRotates = true
// Refactor this
// Global.toastMessage.link = `${walletModel.utilsView.etherscanLink}/${response.result}`
Global.toastMessage.open()
let url = "" //`${walletModel.utilsView.etherscanLink}/${response.result}`
Global.displayToastMessage(qsTr("Transaction pending..."),
"",
"",
true,
Constants.ephemeralNotificationType.normal,
url)
root.close()
} catch (e) {

View File

@ -8,7 +8,6 @@ InviteFriendsPopup 1.0 InviteFriendsPopup.qml
ModalPopup 1.0 ModalPopup.qml
PopupMenu 1.0 PopupMenu.qml
SendModal 1.0 SendModal.qml
ToastMessage 1.0 ToastMessage.qml
TransactionModal 1.0 TransactionModal.qml
TransactionSettingsConfirmationPopup 1.0 TransactionSettingsConfirmationPopup.qml
UnblockContactConfirmationDialog 1.0 UnblockContactConfirmationDialog.qml

View File

@ -180,6 +180,11 @@ QtObject {
}
}
readonly property QtObject ephemeralNotificationType: QtObject {
readonly property int normal: 0
readonly property int success: 1
}
readonly property int communityImported: 0
readonly property int communityImportingInProgress: 1
readonly property int communityImportingError: 2

View File

@ -14,7 +14,6 @@ QtObject {
property var mainModuleInst
property var privacyModuleInst
property var toastMessage
property bool profilePopupOpened: false
property string currentNetworkId: ""
property bool networkGuarded: root.currentNetworkId === Constants.networkMainnet ||
@ -29,6 +28,7 @@ QtObject {
signal openProfilePopupRequested(string publicKey, var parentPopup, bool openNicknamePopup)
signal openChangeProfilePicPopup()
signal displayToastMessage(string title, string subTitle, string icon, bool loading, int ephNotifType, string url)
function openProfilePopup(publicKey, parentPopup, openNicknamePopup){
openProfilePopupRequested(publicKey, parentPopup, openNicknamePopup);