status-desktop/ui/app/AppMain.qml

795 lines
29 KiB
QML
Raw Normal View History

2020-06-17 19:18:31 +00:00
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtMultimedia 5.13
import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.status 1.0
import "./AppLayouts"
import "./AppLayouts/Timeline"
import "./AppLayouts/Wallet"
2021-08-13 20:04:04 +00:00
import "./AppLayouts/WalletV2"
import "./AppLayouts/Chat/popups"
import "./AppLayouts/Chat/popups/community"
import "./AppLayouts/Profile/Sections"
import "./AppLayouts/stores"
2021-06-17 18:41:11 +00:00
import Qt.labs.platform 1.1
import Qt.labs.settings 1.0
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Layout 0.1
import StatusQ.Popups 0.1
import StatusQ.Core 0.1
2021-06-17 18:41:11 +00:00
Item {
id: appMain
anchors.fill: parent
property alias appLayout: appLayout
2021-06-17 18:41:11 +00:00
property var newVersionJSON: JSON.parse(utilsModel.newVersion)
property bool profilePopupOpened: false
property bool networkGuarded: profileModel.network.current === Constants.networkMainnet || (profileModel.network.current === Constants.networkRopsten && localAccountSensitiveSettings.stickersEnsRopsten)
property RootStore rootStore: RootStore { }
signal settingsLoaded()
signal openContactsPopup()
function changeAppSectionBySectionType(sectionType) {
mainModule.setActiveSectionBySectionType(sectionType)
}
function changeAppSectionBySectionId(sectionId) {
mainModule.setActiveSectionById(sectionId)
}
function getProfileImage(pubkey, isCurrentUser, useLargeImage) {
if (isCurrentUser || (isCurrentUser === undefined && pubkey === userProfile.pubKey)) {
return userProfile.thumbnailImage
}
const index = appMain.rootStore.contactsModuleInst.model.list.getContactIndexByPubkey(pubkey)
if (index === -1) {
return
}
if (localAccountSensitiveSettings.onlyShowContactsProfilePics) {
const isContact = appMain.rootStore.contactsModuleInst.model.list.rowData(index, "isContact")
if (isContact === "false") {
return
}
}
return appMain.rootStore.contactsModuleInst.model.list.rowData(index, useLargeImage ? "largeImage" : "thumbnailImage")
}
function openPopup(popupComponent, params = {}) {
const popup = popupComponent.createObject(appMain, params);
popup.open()
return popup
}
function getContactListObject(dataModel) {
const nbContacts = appMain.rootStore.contactsModuleInst.model.list.rowCount()
const contacts = []
let contact
for (let i = 0; i < nbContacts; i++) {
if (appMain.rootStore.contactsModuleInst.model.list.rowData(i, "isBlocked") === "true") {
continue
}
contact = {
name: appMain.rootStore.contactsModuleInst.model.list.rowData(i, "name"),
localNickname: appMain.rootStore.contactsModuleInst.model.list.rowData(i, "localNickname"),
pubKey: appMain.rootStore.contactsModuleInst.model.list.rowData(i, "pubKey"),
address: appMain.rootStore.contactsModuleInst.model.list.rowData(i, "address"),
identicon: appMain.rootStore.contactsModuleInst.model.list.rowData(i, "identicon"),
thumbnailImage: appMain.rootStore.contactsModuleInst.model.list.rowData(i, "thumbnailImage"),
isUser: false,
isContact: appMain.rootStore.contactsModuleInst.model.list.rowData(i, "isContact") !== "false"
}
contacts.push(contact)
if (dataModel) {
dataModel.append(contact);
}
}
return contacts
}
function getUserNickname(pubKey) {
// Get contact nickname
const contactList = appMain.rootStore.contactsModuleInst.model.list
const contactCount = contactList.rowCount()
for (let i = 0; i < contactCount; i++) {
if (contactList.rowData(i, 'pubKey') === pubKey) {
return contactList.rowData(i, 'localNickname')
}
}
return ""
}
function openLink(link) {
if (localAccountSensitiveSettings.showBrowserSelector) {
appMain.openPopup(chooseBrowserPopupComponent, {link: link})
} else {
if (localAccountSensitiveSettings.openLinksInStatus) {
appMain.changeAppSectionBySectionType(Constants.appSection.browser)
browserLayoutContainer.item.openUrlInNewTab(link)
} else {
Qt.openUrlExternally(link)
}
}
}
function openProfilePopup(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, parentPopup){
var popup = profilePopupComponent.createObject(appMain);
if(parentPopup){
popup.parentPopup = parentPopup;
}
popup.openPopup(userProfile.pubKey !== fromAuthorParam, userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam);
profilePopupOpened = true
}
property Component profilePopupComponent: ProfilePopup {
id: profilePopup
store: rootStore
onClosed: {
if(profilePopup.parentPopup){
profilePopup.parentPopup.close();
}
profilePopupOpened = false
destroy()
}
}
2021-06-17 18:41:11 +00:00
Component {
id: downloadModalComponent
DownloadModal {
onClosed: {
destroy();
}
2021-08-13 20:04:04 +00:00
}
}
Audio {
id: errorSound
track: "error.mp3"
}
Audio {
id: sendMessageSound
track: "send_message.wav"
}
Audio {
id: notificationSound
track: "notification.wav"
}
2021-06-17 18:41:11 +00:00
ModuleWarning {
id: versionWarning
width: parent.width
visible: newVersionJSON.available
color: Style.current.green
btnWidth: 100
text: qsTr("A new version of Status (%1) is available").arg(newVersionJSON.version)
btnText: qsTr("Download")
onClick: function(){
openPopup(downloadModalComponent, {newVersionAvailable: newVersionJSON.available, downloadURL: newVersionJSON.url})
}
}
StatusAppLayout {
id: appLayout
2021-06-17 18:41:11 +00:00
width: parent.width
anchors.top: parent.top
anchors.topMargin: versionWarning.visible ? 32 : 0
2021-06-17 18:41:11 +00:00
anchors.bottom: parent.bottom
appNavBar: StatusAppNavBar {
height: appMain.height
communityTypeRole: "sectionType"
communityTypeValue: Constants.appSection.community
sectionModel: mainModule.sectionsModel
property bool communityAdded: false
onAboutToUpdateFilteredRegularModel: {
communityAdded = false
}
filterRegularItem: function(item) {
if(!item.enabled)
return false
if(item.sectionType === Constants.appSection.community)
if(communityAdded)
return false
else
communityAdded = true
return true
}
filterCommunityItem: function(item) {
return item.sectionType === Constants.appSection.community
2021-06-17 18:41:11 +00:00
}
regularNavBarButton: StatusNavBarTabButton {
anchors.horizontalCenter: parent.horizontalCenter
name: model.icon.length > 0? "" : model.name
icon.name: model.icon
icon.source: model.image
tooltip.text: model.name
checked: model.active
badge.value: model.notificationsCount
badge.visible: model.hasNotification
badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusBadge.borderColor
badge.border.width: 2
2021-06-17 18:41:11 +00:00
onClicked: {
changeAppSectionBySectionId(model.id)
2021-06-17 18:41:11 +00:00
}
}
2021-06-17 18:41:11 +00:00
communityNavBarButton: StatusNavBarTabButton {
2021-06-17 18:41:11 +00:00
anchors.horizontalCenter: parent.horizontalCenter
name: model.icon.length > 0? "" : model.name
icon.name: model.icon
icon.source: model.image
2021-06-17 18:41:11 +00:00
tooltip.text: model.name
checked: model.active
badge.value: model.notificationsCount
badge.visible: model.hasNotification
2021-06-17 18:41:11 +00:00
badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusBadge.borderColor
badge.border.width: 2
onClicked: {
changeAppSectionBySectionId(model.id)
}
2021-06-17 18:41:11 +00:00
popupMenu: StatusPopupMenu {
id: communityContextMenu
openHandler: function () {
chatsModel.communities.setObservedCommunity(model.id)
}
StatusMenuItem {
//% "Invite People"
text: qsTrId("invite-people")
icon.name: "share-ios"
enabled: chatsModel.communities.observedCommunity.canManageUsers
onTriggered: openPopup(inviteFriendsToCommunityPopup, {
community: chatsModel.communities.observedCommunity
})
2021-06-17 18:41:11 +00:00
}
StatusMenuItem {
//% "View Community"
text: qsTrId("view-community")
icon.name: "group-chat"
onTriggered: openPopup(communityProfilePopup, {
store: appMain.rootStore,
2021-06-17 18:41:11 +00:00
community: chatsModel.communities.observedCommunity
})
}
StatusMenuItem {
enabled: chatsModel.communities.observedCommunity.admin
//% "Edit Community"
text: qsTrId("edit-community")
icon.name: "edit"
onTriggered: openPopup(editCommunityPopup, {store: appMain.rootStore, community: chatsModel.communities.observedCommunity})
2021-06-17 18:41:11 +00:00
}
StatusMenuSeparator {}
StatusMenuItem {
//% "Leave Community"
text: qsTrId("leave-community")
icon.name: "arrow-right"
icon.width: 14
iconRotation: 180
type: StatusMenuItem.Type.Danger
onTriggered: chatsModel.communities.leaveCommunity(model.id)
}
}
}
navBarProfileButton: StatusNavBarTabButton {
id: profileButton
property bool opened: false
icon.source: profileModule.thumbnailImage || ""
2021-06-17 18:41:11 +00:00
badge.visible: true
badge.anchors.rightMargin: 4
badge.anchors.topMargin: 25
badge.implicitHeight: 15
badge.implicitWidth: 15
badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusAppNavBar.backgroundColor
badge.color: {
return userProfile.sendUserStatus ? Style.current.green : Style.current.midGrey
/*
// Use this code once support for custom user status is added
switch(userProfile.currentUserStatus){
case Constants.statusType_Online:
return Style.current.green;
case Constants.statusType_DoNotDisturb:
return Style.current.red;
default:
return Style.current.midGrey;
}*/
}
2021-06-17 18:41:11 +00:00
badge.border.width: 3
onClicked: {
userStatusContextMenu.opened ?
userStatusContextMenu.close() :
userStatusContextMenu.open()
}
2021-06-17 18:41:11 +00:00
UserStatusContextMenu {
id: userStatusContextMenu
y: profileButton.y - userStatusContextMenu.height
store: appMain.rootStore
2021-06-17 18:41:11 +00:00
}
}
}
2021-06-17 18:41:11 +00:00
appView: StackLayout {
id: appView
anchors.fill: parent
currentIndex: {
if(mainModule.activeSection.sectionType === Constants.appSection.chat) {
/*************************************/
// This will be refactored later
if (chatsModel.communities.activeCommunity.active) {
chatLayoutContainer.chatColumn.hideChatInputExtendedArea();
chatsModel.communities.activeCommunity.active = false
}
/*************************************/
return 0
}
else if(mainModule.activeSection.sectionType === Constants.appSection.community) {
/*************************************/
// This will be refactored later
chatsModel.communities.setActiveCommunity(mainModule.activeSection.id)
/*************************************/
return 99 //Don't know why, but that's how it was in Utils::getAppSectionIndex function.
}
else if(mainModule.activeSection.sectionType === Constants.appSection.wallet) {
return 1
}
else if(mainModule.activeSection.sectionType === Constants.appSection.walletv2) {
return 7
}
else if(mainModule.activeSection.sectionType === Constants.appSection.browser) {
return 2
}
else if(mainModule.activeSection.sectionType === Constants.appSection.timeline) {
return 3
}
else if(mainModule.activeSection.sectionType === Constants.appSection.profile) {
return 4
}
else if(mainModule.activeSection.sectionType === Constants.appSection.node) {
return 7
}
// We should never end up here
console.error("Unknown section type")
}
2021-06-17 18:41:11 +00:00
onCurrentIndexChanged: {
var obj = this.children[currentIndex];
if(!obj)
return
if (obj.onActivated && typeof obj.onActivated === "function") {
2021-06-17 18:41:11 +00:00
this.children[currentIndex].onActivated()
}
if(obj === browserLayoutContainer && browserLayoutContainer.active == false){
2021-06-17 18:41:11 +00:00
browserLayoutContainer.active = true;
}
timelineLayoutContainer.active = obj === timelineLayoutContainer
if(obj === walletLayoutContainer){
2021-06-17 18:41:11 +00:00
walletLayoutContainer.showSigningPhrasePopup();
}
if(obj === walletV2LayoutContainer){
2021-06-17 18:41:11 +00:00
walletV2LayoutContainer.showSigningPhrasePopup();
}
}
ChatLayout {
id: chatLayoutContainer
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
messageStore: appMain.rootStore.messageStore
2021-06-17 18:41:11 +00:00
onProfileButtonClicked: {
appMain.changeAppSectionBySectionType(Constants.appSection.profile);
2021-06-17 18:41:11 +00:00
}
}
WalletLayout {
id: walletLayoutContainer
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
}
Component {
id: browserLayoutComponent
BrowserLayout {
globalStore: appMain.rootStore
}
2021-06-17 18:41:11 +00:00
}
Loader {
id: browserLayoutContainer
sourceComponent: browserLayoutComponent
active: false
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
// Loaders do not have access to the context, so props need to be set
// Adding a "_" to avoid a binding loop
property var _chatsModel: chatsModel.messageView
property var _walletModel: walletModel
property var _utilsModel: utilsModel
property var _web3Provider: web3Provider
}
Loader {
id: timelineLayoutContainer
sourceComponent: Component {
TimelineLayout {
messageStore: appMain.rootStore.messageStore
rootStore: appMain.rootStore
}
}
onLoaded: timelineLayoutContainer.item.onActivated()
active: false
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
}
2021-06-17 18:41:11 +00:00
ProfileLayout {
id: profileLayoutContainer
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
globalStore: appMain.rootStore
2021-06-17 18:41:11 +00:00
}
NodeLayout {
id: nodeLayoutContainer
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
}
UIComponents {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
}
WalletV2Layout {
id: walletV2LayoutContainer
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
}
}
Connections {
target: chatsModel
onNotificationClicked: {
applicationWindow.makeStatusAppActive()
switch(notificationType){
case Constants.osNotificationType.newContactRequest:
appView.currentIndex = Utils.getAppSectionIndex(Constants.chat)
appMain.openContactsPopup()
break
case Constants.osNotificationType.acceptedContactRequest:
appView.currentIndex = Utils.getAppSectionIndex(Constants.chat)
break
case Constants.osNotificationType.joinCommunityRequest:
case Constants.osNotificationType.acceptedIntoCommunity:
case Constants.osNotificationType.rejectedByCommunity:
appView.currentIndex = Utils.getAppSectionIndex(Constants.community)
break
case Constants.osNotificationType.newMessage:
appView.currentIndex = Utils.getAppSectionIndex(Constants.chat)
break
}
}
}
2021-06-17 18:41:11 +00:00
Connections {
target: profileModel
ignoreUnknownSignals: true
enabled: removeMnemonicAfterLogin
onInitialized: {
mnemonicModule.remove()
}
}
Connections {
target: appMain.rootStore.contactsModuleInst.model
2021-06-17 18:41:11 +00:00
onContactRequestAdded: {
if (!localAccountSensitiveSettings.notifyOnNewRequests) {
2021-06-17 18:41:11 +00:00
return
}
const isContact = appMain.rootStore.contactsModuleInst.model.isAdded(address)
// Note:
// Whole this Connection object should be moved to the nim side.
// Left here only cause we don't have a way to deal with translations on the nim side.
const title = isContact ? qsTrId("contact-request-accepted") :
//% "New contact request"
qsTrId("new-contact-request")
const message = //% "You can now chat with %1"
isContact ? qsTrId("you-can-now-chat-with--1").arg(Utils.removeStatusEns(name)) :
//% "%1 requests to become contacts"
qsTrId("-1-requests-to-become-contacts").arg(Utils.removeStatusEns(name))
if (Qt.platform.os === "linux") {
// Linux Notifications are not implemented in Nim/C++ yet
return systemTray.showMessage(title, message, systemTray.icon.source, 4000)
}
2021-06-17 18:41:11 +00:00
//% "Contact request accepted"
profileModel.showOSNotification(title,
message,
isContact? Constants.osNotificationType.acceptedContactRequest :
Constants.osNotificationType.newContactRequest,
localAccountSensitiveSettings.useOSNotifications)
}
}
2021-06-17 18:41:11 +00:00
Component {
id: chooseBrowserPopupComponent
ChooseBrowserPopup {
onClosed: {
destroy()
}
}
}
2021-06-17 18:41:11 +00:00
Component {
id: inviteFriendsToCommunityPopup
InviteFriendsToCommunityPopup {
anchors.centerIn: parent
hasAddedContacts: appMain.rootStore.allContacts.hasAddedContacts()
2021-06-17 18:41:11 +00:00
onClosed: {
destroy()
}
}
}
2021-06-17 18:41:11 +00:00
Component {
id: communityProfilePopup
2021-06-17 18:41:11 +00:00
CommunityProfilePopup {
anchors.centerIn: parent
onClosed: {
destroy()
}
}
}
2021-06-17 18:41:11 +00:00
Component {
id: editCommunityPopup
CreateCommunityPopup {
anchors.centerIn: parent
isEdit: true
onClosed: {
destroy()
}
}
}
2021-06-17 18:41:11 +00:00
Component {
id: editChannelPopup
CreateChannelPopup {
anchors.centerIn: parent
isEdit: true
pinnedMessagesPopupComponent: chatLayoutContainer.chatColumn.pinnedMessagesPopupComponent
onClosed: {
destroy()
}
}
}
2021-06-17 18:41:11 +00:00
Component {
id: genericConfirmationDialog
ConfirmationDialog {
onClosed: {
destroy()
}
}
}
2021-06-17 18:41:11 +00:00
ToastMessage {
id: toastMessage
}
2021-07-05 12:34:56 +00:00
// Add SendModal here as it is used by the Wallet as well as the Browser
2021-06-17 18:41:11 +00:00
Loader {
id: sendModal
active: false
2021-07-05 12:34:56 +00:00
2021-06-17 18:41:11 +00:00
function open() {
this.active = true
this.item.open()
}
2021-06-17 18:41:11 +00:00
function closed() {
// this.sourceComponent = undefined // kill an opened instance
this.active = false
}
2021-06-17 18:41:11 +00:00
sourceComponent: SendModal {
store: appMain.rootStore
2021-06-17 18:41:11 +00:00
onOpened: {
2021-07-05 12:34:56 +00:00
walletModel.gasView.getGasPrice()
2021-06-17 18:41:11 +00:00
}
onClosed: {
sendModal.closed()
}
}
}
2021-06-17 18:41:11 +00:00
Action {
shortcut: "Ctrl+1"
onTriggered: changeAppSectionBySectionType(Constants.appSection.chat)
}
2021-06-17 18:41:11 +00:00
Action {
shortcut: "Ctrl+2"
onTriggered: changeAppSectionBySectionType(Constants.appSection.browser)
}
2021-06-17 18:41:11 +00:00
Action {
shortcut: "Ctrl+3"
onTriggered: changeAppSectionBySectionType(Constants.appSection.wallet)
2021-06-17 18:41:11 +00:00
}
Action {
shortcut: "Ctrl+4, Ctrl+,"
onTriggered: changeAppSectionBySectionType(Constants.appSection.profile)
2021-06-17 18:41:11 +00:00
}
Action {
shortcut: "Ctrl+K"
onTriggered: {
if (channelPicker.opened) {
channelPicker.close()
} else {
channelPicker.open()
}
}
}
Component {
id: statusSmartIdenticonComponent
StatusSmartIdenticon {
property string imageSource: ""
image: StatusImageSettings {
width: channelPicker.imageWidth
height: channelPicker.imageHeight
source: imageSource
isIdenticon: true
}
icon: StatusIconSettings {
width: channelPicker.imageWidth
height: channelPicker.imageHeight
letterSize: 15
color: Theme.palette.miscColor5
}
}
2021-06-17 18:41:11 +00:00
}
StatusInputListPopup {
id: channelPicker
//% "Where do you want to go?"
title: qsTrId("where-do-you-want-to-go-")
showSearchBox: true
width: 350
x: parent.width / 2 - width / 2
y: parent.height / 2 - height / 2
modelList: chatsModel.channelView.chats
getText: function (modelData) {
return modelData.name
}
getImageComponent: function (parent, modelData) {
return statusSmartIdenticonComponent.createObject(parent, {
imageSource: modelData.identicon,
name: modelData.name
2021-06-17 18:41:11 +00:00
});
}
onClicked: function (index) {
appMain.changeAppSectionBySectionType(Constants.appSection.chat)
2021-06-17 18:41:11 +00:00
chatsModel.channelView.setActiveChannelByIndex(index)
channelPicker.close()
}
}
}
Component.onCompleted: {
// Since https://github.com/status-im/status-desktop/commit/93668ff75
// we're hiding the setting to change appearance for compact normal mode
// of the UI. For now, compact mode is the new default.
//
// Prior to this change, most likely many users are still using the
// normal mode configuration, so we have to enforce compact mode for
// those.
if (!localAccountSensitiveSettings.useCompactMode) {
localAccountSensitiveSettings.useCompactMode = true
}
const whitelist = profileModel.getLinkPreviewWhitelist()
try {
const whiteListedSites = JSON.parse(whitelist)
let settingsUpdated = false
// Add Status links to whitelist
whiteListedSites.push({title: "Status", address: Constants.deepLinkPrefix, imageSite: false})
whiteListedSites.push({title: "Status", address: Constants.joinStatusLink, imageSite: false})
let settings = localAccountSensitiveSettings.whitelistedUnfurlingSites
if (!settings) {
settings = {}
}
// Set Status links as true. We intercept thoseURLs so it is privacy-safe
if (!settings[Constants.deepLinkPrefix] || !settings[Constants.joinStatusLink]) {
settings[Constants.deepLinkPrefix] = true
settings[Constants.joinStatusLink] = true
settingsUpdated = true
}
const whitelistedHostnames = []
// Add whitelisted sites in to app settings that are not already there
whiteListedSites.forEach(site => {
if (!settings.hasOwnProperty(site.address)) {
settings[site.address] = false
settingsUpdated = true
}
whitelistedHostnames.push(site.address)
})
// Remove any whitelisted sites from app settings that don't exist in the
// whitelist from status-go
Object.keys(settings).forEach(settingsHostname => {
if (!whitelistedHostnames.includes(settingsHostname)) {
delete settings[settingsHostname]
settingsUpdated = true
}
})
if (settingsUpdated) {
localAccountSensitiveSettings.whitelistedUnfurlingSites = settings
}
} catch (e) {
console.error('Could not parse the whitelist for sites', e)
}
appMain.settingsLoaded()
}
}