diff --git a/.gitmodules b/.gitmodules index 0627b0b4b3..cc3b297938 100644 --- a/.gitmodules +++ b/.gitmodules @@ -103,3 +103,6 @@ [submodule "ui/StatusQ"] path = ui/StatusQ url = https://github.com/status-im/StatusQ +[submodule "vendor/semver.nim"] + path = vendor/semver.nim + url = https://github.com/euantorano/semver.nim diff --git a/Makefile b/Makefile index 6a328a899c..9e76769257 100644 --- a/Makefile +++ b/Makefile @@ -164,6 +164,12 @@ endif NIM_PARAMS += --outdir:./bin + +# App version +VERSIONFILE=VERSION +DESKTOP_VERSION=`cat $(VERSIONFILE)` +NIM_PARAMS += -d:DESKTOP_VERSION="$(DESKTOP_VERSION)" + $(DOTHERSIDE): | deps echo -e $(BUILD_MSG) "DOtherSide" + cd vendor/DOtherSide && \ diff --git a/src/app/utilsView/core.nim b/src/app/utilsView/core.nim index cd24a7511f..130e7b6f3c 100644 --- a/src/app/utilsView/core.nim +++ b/src/app/utilsView/core.nim @@ -24,4 +24,4 @@ proc delete*(self: UtilsController) = delete self.view proc init*(self: UtilsController) = - discard + self.view.asyncCheckForUpdates() diff --git a/src/app/utilsView/view.nim b/src/app/utilsView/view.nim index 8c5d167763..37007c8e21 100644 --- a/src/app/utilsView/view.nim +++ b/src/app/utilsView/view.nim @@ -1,6 +1,7 @@ import NimQml, os, strformat, strutils, parseUtils, chronicles import stint -import ../../status/[status, wallet, settings] +import ../../status/[status, wallet, settings, updates] +import ../../status/tasks/[qt, task_runner_impl] import ../../status/stickers import ../../status/tokens as status_tokens import ../../status/types @@ -9,13 +10,25 @@ import ../../status/ens as status_ens import ../utils/image_utils import web3/[ethtypes, conversions] import stew/byteutils +import json + +const DESKTOP_VERSION {.strdefine.} = "0.0.0" + +type CheckForNewVersionTaskArg = ref object of QObjectTaskArg + QtObject: type UtilsView* = ref object of QObject status*: Status + newVersion*: string proc setup(self: UtilsView) = self.QObject.setup + self.newVersion = $(%*{ + "available": false, + "version": "0.0.0", + "url": "about:blank" + }) proc delete*(self: UtilsView) = self.QObject.delete @@ -112,4 +125,67 @@ QtObject: close(f) else: raise newException(IOError, "cannot open: " & filename) - \ No newline at end of file + + proc getCurrentVersion*(self: UtilsView): string {.slot.} = + return DESKTOP_VERSION + + proc newVersionChanged(self: UtilsView) {.signal.} + + proc getLatestVersionJSON(): string = + var version = "" + var url = "" + + try: + debug "Getting latest version information" + let latestVersion = getLatestVersion() + version = latestVersion.version + url = latestVersion.url + except Exception as e: + error "Error while getting latest version information", msg = e.msg + + result = $(%*{ + "version": version, + "url": url + }) + + const checkForUpdatesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + debug "Check for updates - async" + let arg = decode[CheckForNewVersionTaskArg](argEncoded) + arg.finish(getLatestVersionJSON()) + + proc asyncRequestLatestVersion[T](self: T, slot: string) = + let arg = CheckForNewVersionTaskArg( + tptr: cast[ByteAddress](checkForUpdatesTask), + vptr: cast[ByteAddress](self.vptr), + slot: slot + ) + self.status.tasks.threadpool.start(arg) + + proc latestVersionSuccess*(self: UtilsView, latestVersionJSON: string) {.slot.} = + let latestVersionObj = parseJSON(latestVersionJSON) + let latestVersion = latestVersionObj{"version"}.getStr() + if latestVersion == "": return + + let available = isNewer(DESKTOP_VERSION, latestVersion) + latestVersionObj["available"] = newJBool(available) + debug "New version?", available, info=latestVersion + + self.newVersion = $(%*latestVersionObj) + self.newVersionChanged() + + proc checkForUpdates*(self: UtilsView) {.slot.} = + if self.status.settings.getCurrentNetwork() != Network.Mainnet: return + debug "Check for updates - sync" + self.latestVersionSuccess(getLatestVersionJSON()) + + proc asyncCheckForUpdates*(self: UtilsView) {.slot.} = + if self.status.settings.getCurrentNetwork() != Network.Mainnet: return + self.asyncRequestLatestVersion("latestVersionSuccess") + + proc getNewVersion*(self: UtilsView): string {.slot.} = + return self.newVersion + + QtProperty[string] newVersion: + read = getNewVersion + notify = newVersionChanged + diff --git a/src/status/constants.nim b/src/status/constants.nim index b27228623c..ee4d527ceb 100644 --- a/src/status/constants.nim +++ b/src/status/constants.nim @@ -5,3 +5,5 @@ export STATUSGODIR export KEYSTOREDIR export TMPDIR export LOGDIR + +const APP_UPDATES_ENS* = "desktop.status.eth" diff --git a/src/status/provider.nim b/src/status/provider.nim index 59f1abdaba..60e80f6666 100644 --- a/src/status/provider.nim +++ b/src/status/provider.nim @@ -11,11 +11,11 @@ import nbaser import stew/byteutils from base32 import nil -const HTTPS_SCHEME = "https" -const IPFS_GATEWAY = ".infura.status.im" -const SWARM_GATEWAY = "swarm-gateways.net" +const HTTPS_SCHEME* = "https" +const IPFS_GATEWAY* = ".infura.status.im" +const SWARM_GATEWAY* = "swarm-gateways.net" -const base58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +const base58* = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" logScope: topics = "provider-model" diff --git a/src/status/updates.nim b/src/status/updates.nim new file mode 100644 index 0000000000..3de5d5a974 --- /dev/null +++ b/src/status/updates.nim @@ -0,0 +1,44 @@ +import ens, provider +import stew/byteutils +from base32 import nil +import chronicles, httpclient, net +import nbaser, strutils +import semver +import constants + + +type + VersionInfo* = object + version*: string + url*: string + +proc getLatestVersion*(): VersionInfo = + let contentHash = contenthash(APP_UPDATES_ENS) + if contentHash == "": + raise newException(ValueError, "ENS does not have a content hash") + + var url: string = "" + + let decodedHash = contentHash.decodeENSContentHash() + case decodedHash[0]: + of ENSType.IPFS: + let base32Hash = base32.encode(string.fromBytes(base58.decode(decodedHash[1]))).toLowerAscii().replace("=", "") + url = "https://" & base32Hash & IPFS_GATEWAY + of ENSType.SWARM: + url = "https://" & SWARM_GATEWAY & "/bzz:/" & decodedHash[1] + of ENSType.IPNS: + url = "https://" & decodedHash[1] + else: + warn "Unknown content for", contentHash + raise newException(ValueError, "Unknown content for " & contentHash) + + # Read version from folder + let secureSSLContext = newContext() + let client = newHttpClient(sslContext = secureSSLContext) + result.version = client.getContent(url & "/VERSION" ).strip() + result.url = url + +proc isNewer*(currentVersion, versionToCheck: string): bool = + let lastVersion = parseVersion(versionToCheck) + let currVersion = parseVersion(currentVersion) + result = lastVersion > currVersion diff --git a/ui/app/AppLayouts/Profile/Sections/AboutContainer.qml b/ui/app/AppLayouts/Profile/Sections/AboutContainer.qml index caa27d153c..2ff47cdae4 100644 --- a/ui/app/AppLayouts/Profile/Sections/AboutContainer.qml +++ b/ui/app/AppLayouts/Profile/Sections/AboutContainer.qml @@ -23,7 +23,7 @@ Item { //% "App version" name: qsTrId("version") //% "Version: %1" - description: qsTrId("version---1").arg("0.1.0-beta.11") + description: qsTrId("version---1").arg(utilsModel.getCurrentVersion()) tooltipUnder: true } @@ -50,7 +50,8 @@ Item { parent.font.underline = false } onClicked: { - appMain.openLink("https://github.com/status-im/nim-status-client/releases") + utilsModel.checkForUpdates(); + openPopup(downloadModalComponent, {newVersionAvailable: newVersionJSON.available, downloadURL: newVersionJSON.url}) } } } diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index 803556cfba..8e73336658 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -3,8 +3,8 @@ import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 import "../../../imports" import "../../../shared" +import "../Profile/Sections" import "." - import StatusQ.Layout 0.1 Item { @@ -21,17 +21,9 @@ Item { SignPhraseModal { id: signPhrasePopup } - - SeedPhraseBackupWarning { - id: seedPhraseWarning - width: parent.width - anchors.top: parent.top - } StatusAppTwoPanelLayout { - anchors.top: seedPhraseWarning.bottom - height: walletView.height - seedPhraseWarning.height - width: walletView.width + anchors.fill: parent Component.onCompleted: { if(onboardingModel.firstTimeLogin){ diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index afd34efd15..2f9fd5154d 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -12,6 +12,8 @@ import "./AppLayouts/Wallet" import "./AppLayouts/WalletV2" import "./AppLayouts/Chat/components" import "./AppLayouts/Chat/CommunityComponents" +import "./AppLayouts/Profile/Sections" + import Qt.labs.platform 1.1 import Qt.labs.settings 1.0 @@ -20,12 +22,13 @@ import StatusQ.Controls 0.1 import StatusQ.Layout 0.1 import StatusQ.Popups 0.1 -StatusAppLayout { + +Item { id: appMain anchors.fill: parent property alias appSettings: appSettings - + property var newVersionJSON: JSON.parse(utilsModel.newVersion) property bool profilePopupOpened: false property bool networkGuarded: profileModel.network.current === Constants.networkMainnet || (profileModel.network.current === Constants.networkRopsten && appSettings.stickersEnsRopsten) @@ -136,296 +139,13 @@ StatusAppLayout { } } - - appNavBar: StatusAppNavBar { - height: appMain.height - - navBarChatButton: StatusNavBarTabButton { - icon.name: "chat" - checked: !chatsModel.communities.activeCommunity.active && appView.currentIndex === Utils.getAppSectionIndex(Constants.chat) - //% "Chat" - tooltip.text: qsTrId("chat") - badge.value: chatsModel.messageView.unreadDirectMessagesAndMentionsCount + profileModel.contacts.contactRequests.count - badge.visible: badge.value > 0 || (chatsModel.messageView.unreadMessagesCount > 0 && !checked) - badge.anchors.rightMargin: badge.value > 0 ? 0 : 4 - badge.anchors.topMargin: badge.value > 0 ? 4 : 5 - badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusAppNavBar.backgroundColor - badge.border.width: 2 - onClicked: { - if (chatsModel.communities.activeCommunity.active) { - chatLayoutContainer.chatColumn.input.hideExtendedArea(); - chatsModel.communities.activeCommunity.active = false - } - appMain.changeAppSection(Constants.chat) + Component { + id: downloadModalComponent + DownloadModal { + onClosed: { + destroy(); } } - - navBarCommunityTabButtons.model: appSettings.communitiesEnabled && chatsModel.communities.joinedCommunities - navBarCommunityTabButtons.delegate: StatusNavBarTabButton { - onClicked: { - appMain.changeAppSection(Constants.chat) - chatsModel.communities.setActiveCommunity(model.id) - } - - anchors.horizontalCenter: parent.horizontalCenter - - checked: chatsModel.communities.activeCommunity.active && chatsModel.communities.activeCommunity.id === model.id - name: model.name - tooltip.text: model.name - icon.color: model.communityColor - icon.source: model.thumbnailImage - - badge.value: model.unviewedMentionsCount + model.requestsCount - badge.visible: badge.value > 0 || (!checked && model.unviewedMessagesCount > 0) - badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusBadge.borderColor - badge.border.width: 2 - badge.anchors.rightMargin: 4 - badge.anchors.topMargin: 5 - - 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 - }) - } - - StatusMenuItem { - //% "View Community" - text: qsTrId("view-community") - icon.name: "group-chat" - onTriggered: openPopup(communityProfilePopup, { - community: chatsModel.communities.observedCommunity - }) - } - - StatusMenuItem { - enabled: chatsModel.communities.observedCommunity.admin - //% "Edit Community" - text: qsTrId("edit-community") - icon.name: "edit" - onTriggered: openPopup(editCommunityPopup, {community: chatsModel.communities.observedCommunity}) - } - - 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) - } - } - } - - navBarTabButtons: [ - StatusNavBarTabButton { - icon.name: "wallet" - //% "Wallet" - tooltip.text: qsTrId("wallet") - visible: enabled - enabled: isExperimental === "1" || appSettings.isWalletEnabled - checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.wallet) - onClicked: appMain.changeAppSection(Constants.wallet) - }, - - StatusNavBarTabButton { - icon.name: "wallet" - tooltip.text: qsTr("Wallet v2") - visible: enabled - enabled: isExperimental === "1" || appSettings.isWalletV2Enabled - checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.walletv2) - onClicked: appMain.changeAppSection(Constants.walletv2) - }, - - StatusNavBarTabButton { - enabled: isExperimental === "1" || appSettings.isBrowserEnabled - visible: enabled - //% "Browser" - tooltip.text: qsTrId("browser") - icon.name: "browser" - checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.browser) - onClicked: appMain.changeAppSection(Constants.browser) - }, - - StatusNavBarTabButton { - enabled: isExperimental === "1" || appSettings.timelineEnabled - visible: enabled - //% "Timeline" - tooltip.text: qsTrId("timeline") - icon.name: "status-update" - checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.timeline) - onClicked: appMain.changeAppSection(Constants.timeline) - }, - - StatusNavBarTabButton { - enabled: isExperimental === "1" || appSettings.nodeManagementEnabled - visible: enabled - tooltip.text: qsTr("Node Management") - icon.name: "node" - checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.node) - onClicked: appMain.changeAppSection(Constants.node) - }, - - StatusNavBarTabButton { - id: profileBtn - //% "Profile" - tooltip.text: qsTrId("profile") - icon.name: "profile" - checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.profile) - onClicked: appMain.changeAppSection(Constants.profile) - - badge.visible: !profileModel.mnemonic.isBackedUp && !checked - badge.anchors.rightMargin: 4 - badge.anchors.topMargin: 5 - badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusAppNavBar.backgroundColor - badge.border.width: 2 - } - ] - - navBarProfileButton: StatusNavBarTabButton { - id: profileButton - icon.source: profileModel.profile.thumbnailImage || "" - 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 profileModel.profile.sendUserStatus ? Style.current.green : Style.current.midGrey - /* - // Use this code once support for custom user status is added - switch(profileModel.profile.currentUserStatus){ - case Constants.statusType_Online: - return Style.current.green; - case Constants.statusType_DoNotDisturb: - return Style.current.red; - default: - return Style.current.midGrey; - }*/ - } - badge.border.width: 3 - onClicked: userStatusContextMenu.open() - - UserStatusContextMenu { - id: userStatusContextMenu - y: profileButton.y - userStatusContextMenu.height - } - } - } - - appView: StackLayout { - id: appView - anchors.fill: parent - currentIndex: 0 - onCurrentIndexChanged: { - if (typeof this.children[currentIndex].onActivated === "function") { - this.children[currentIndex].onActivated() - } - - if(this.children[currentIndex] === browserLayoutContainer && browserLayoutContainer.active == false){ - browserLayoutContainer.active = true; - } - - timelineLayoutContainer.active = this.children[currentIndex] === timelineLayoutContainer - - if(this.children[currentIndex] === walletLayoutContainer){ - walletLayoutContainer.showSigningPhrasePopup(); - } - - if(this.children[currentIndex] === walletV2LayoutContainer){ - walletV2LayoutContainer.showSigningPhrasePopup(); - } - } - - ChatLayout { - id: chatLayoutContainer - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.fillHeight: true - onProfileButtonClicked: { - appMain.changeAppSection(Constants.profile); - } - } - - WalletLayout { - id: walletLayoutContainer - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.fillHeight: true - } - - Component { - id: browserLayoutComponent - BrowserLayout { } - } - - 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 {} - } - onLoaded: timelineLayoutContainer.item.onActivated() - active: false - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.fillHeight: true - } - - ProfileLayout { - id: profileLayoutContainer - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.fillHeight: true - } - - 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 - } } Settings { @@ -507,252 +227,576 @@ StatusAppLayout { muted: !appSettings.notificationSoundsEnabled } - Connections { - target: profileModel.settings - onGlobalSettingsFileChanged: { - profileModel.changeLocale(globalSettings.locale) + 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}) } - onSettingsFileChanged: { - // 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 (!appSettings.useCompactMode) { - appSettings.useCompactMode = true + } + + ModuleWarning { + id: mnemonicBackupWarning + width: parent.width + visible: appSettings.isWalletEnabled && !profileModel.mnemonic.isBackedUp && appView.currentIndex == Utils.getAppSectionIndex(Constants.wallet) + color: Style.current.red + //% "Back up your seed phrase" + text: qsTrId("back-up-your-seed-phrase") + btnText: qsTr("Back up") + onClick: function() { + openPopup(backupSeedModalComponent); + } + } + + StatusAppLayout { + width: parent.width + anchors.top: parent.top + anchors.topMargin: (versionWarning.visible || mnemonicBackupWarning.visible) ? 32 : 0 + anchors.bottom: parent.bottom + + appNavBar: StatusAppNavBar { + height: appMain.height + + navBarChatButton: StatusNavBarTabButton { + icon.name: "chat" + checked: !chatsModel.communities.activeCommunity.active && appView.currentIndex === Utils.getAppSectionIndex(Constants.chat) + //% "Chat" + tooltip.text: qsTrId("chat") + badge.value: chatsModel.messageView.unreadDirectMessagesAndMentionsCount + profileModel.contacts.contactRequests.count + badge.visible: badge.value > 0 || (chatsModel.messageView.unreadMessagesCount > 0 && !checked) + badge.anchors.rightMargin: badge.value > 0 ? 0 : 4 + badge.anchors.topMargin: badge.value > 0 ? 4 : 5 + badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusAppNavBar.backgroundColor + badge.border.width: 2 + onClicked: { + if (chatsModel.communities.activeCommunity.active) { + chatLayoutContainer.chatColumn.input.hideExtendedArea(); + chatsModel.communities.activeCommunity.active = false + } + appMain.changeAppSection(Constants.chat) + } } - 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}) - const settings = appSettings.whitelistedUnfurlingSites - - // 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 + navBarCommunityTabButtons.model: appSettings.communitiesEnabled && chatsModel.communities.joinedCommunities + navBarCommunityTabButtons.delegate: StatusNavBarTabButton { + onClicked: { + appMain.changeAppSection(Constants.chat) + chatsModel.communities.setActiveCommunity(model.id) } - const whitelistedHostnames = [] + anchors.horizontalCenter: parent.horizontalCenter - // 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] + checked: chatsModel.communities.activeCommunity.active && chatsModel.communities.activeCommunity.id === model.id + name: model.name + tooltip.text: model.name + icon.color: model.communityColor + icon.source: model.thumbnailImage + + badge.value: model.unviewedMentionsCount + model.requestsCount + badge.visible: badge.value > 0 || (!checked && model.unviewedMessagesCount > 0) + badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusBadge.borderColor + badge.border.width: 2 + badge.anchors.rightMargin: 4 + badge.anchors.topMargin: 5 + + 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 + }) + } + + StatusMenuItem { + //% "View Community" + text: qsTrId("view-community") + icon.name: "group-chat" + onTriggered: openPopup(communityProfilePopup, { + community: chatsModel.communities.observedCommunity + }) + } + + StatusMenuItem { + enabled: chatsModel.communities.observedCommunity.admin + //% "Edit Community" + text: qsTrId("edit-community") + icon.name: "edit" + onTriggered: openPopup(editCommunityPopup, {community: chatsModel.communities.observedCommunity}) + } + + 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) + } + } + } + + navBarTabButtons: [ + StatusNavBarTabButton { + icon.name: "wallet" + //% "Wallet" + tooltip.text: qsTrId("wallet") + visible: enabled + enabled: isExperimental === "1" || appSettings.isWalletEnabled + checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.wallet) + onClicked: appMain.changeAppSection(Constants.wallet) + }, + + StatusNavBarTabButton { + icon.name: "wallet" + tooltip.text: qsTr("Wallet v2") + visible: enabled + enabled: isExperimental === "1" || appSettings.isWalletV2Enabled + checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.walletv2) + onClicked: appMain.changeAppSection(Constants.walletv2) + }, + + StatusNavBarTabButton { + enabled: isExperimental === "1" || appSettings.isBrowserEnabled + visible: enabled + //% "Browser" + tooltip.text: qsTrId("browser") + icon.name: "browser" + checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.browser) + onClicked: appMain.changeAppSection(Constants.browser) + }, + + StatusNavBarTabButton { + enabled: isExperimental === "1" || appSettings.timelineEnabled + visible: enabled + //% "Timeline" + tooltip.text: qsTrId("timeline") + icon.name: "status-update" + checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.timeline) + onClicked: appMain.changeAppSection(Constants.timeline) + }, + + StatusNavBarTabButton { + enabled: isExperimental === "1" || appSettings.nodeManagementEnabled + visible: enabled + tooltip.text: qsTr("Node Management") + icon.name: "node" + checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.node) + onClicked: appMain.changeAppSection(Constants.node) + }, + + StatusNavBarTabButton { + id: profileBtn + //% "Profile" + tooltip.text: qsTrId("profile") + icon.name: "profile" + checked: appView.currentIndex == Utils.getAppSectionIndex(Constants.profile) + onClicked: appMain.changeAppSection(Constants.profile) + + badge.visible: !profileModel.mnemonic.isBackedUp && !checked + badge.anchors.rightMargin: 4 + badge.anchors.topMargin: 5 + badge.border.color: hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusAppNavBar.backgroundColor + badge.border.width: 2 + } + ] + + navBarProfileButton: StatusNavBarTabButton { + id: profileButton + icon.source: profileModel.profile.thumbnailImage || "" + 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 profileModel.profile.sendUserStatus ? Style.current.green : Style.current.midGrey + /* + // Use this code once support for custom user status is added + switch(profileModel.profile.currentUserStatus){ + case Constants.statusType_Online: + return Style.current.green; + case Constants.statusType_DoNotDisturb: + return Style.current.red; + default: + return Style.current.midGrey; + }*/ + } + badge.border.width: 3 + onClicked: userStatusContextMenu.open() + + UserStatusContextMenu { + id: userStatusContextMenu + y: profileButton.y - userStatusContextMenu.height + } + } + } + + appView: StackLayout { + id: appView + anchors.fill: parent + currentIndex: 0 + onCurrentIndexChanged: { + if (typeof this.children[currentIndex].onActivated === "function") { + this.children[currentIndex].onActivated() + } + + if(this.children[currentIndex] === browserLayoutContainer && browserLayoutContainer.active == false){ + browserLayoutContainer.active = true; + } + + timelineLayoutContainer.active = this.children[currentIndex] === timelineLayoutContainer + + if(this.children[currentIndex] === walletLayoutContainer){ + walletLayoutContainer.showSigningPhrasePopup(); + } + + if(this.children[currentIndex] === walletV2LayoutContainer){ + walletV2LayoutContainer.showSigningPhrasePopup(); + } + } + + ChatLayout { + id: chatLayoutContainer + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillHeight: true + onProfileButtonClicked: { + appMain.changeAppSection(Constants.profile); + } + } + + WalletLayout { + id: walletLayoutContainer + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillHeight: true + } + + Component { + id: browserLayoutComponent + BrowserLayout { } + } + + 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 {} + } + onLoaded: timelineLayoutContainer.item.onActivated() + active: false + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillHeight: true + } + + ProfileLayout { + id: profileLayoutContainer + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillHeight: true + } + + 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: profileModel.settings + onGlobalSettingsFileChanged: { + profileModel.changeLocale(globalSettings.locale) + } + onSettingsFileChanged: { + // 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 (!appSettings.useCompactMode) { + appSettings.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}) + const settings = appSettings.whitelistedUnfurlingSites + + // 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 } - }) - if (settingsUpdated) { - appSettings.whitelistedUnfurlingSites = settings + + 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) { + appSettings.whitelistedUnfurlingSites = settings + } + } catch (e) { + console.error('Could not parse the whitelist for sites', e) } - } catch (e) { - console.error('Could not parse the whitelist for sites', e) - } - appMain.settingsLoaded() - } - } - - Connections { - target: profileModel - ignoreUnknownSignals: true - enabled: removeMnemonicAfterLogin - onInitialized: { - profileModel.mnemonic.remove() - } - } - - Connections { - target: profileModel.contacts - onContactRequestAdded: { - if (!appSettings.notifyOnNewRequests) { - return - } - const isContact = profileModel.contacts.isAdded(address) - //% "Contact request accepted" - systemTray.showMessage(isContact ? qsTrId("contact-request-accepted") : - //% "New contact request" - qsTrId("new-contact-request"), - //% "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)), - SystemTrayIcon.NoIcon, - Constants.notificationPopupTTL) - } - } - - Component { - id: chooseBrowserPopupComponent - ChooseBrowserPopup { - onClosed: { - destroy() + appMain.settingsLoaded() } } - } - Component { - id: inviteFriendsToCommunityPopup - InviteFriendsToCommunityPopup { - anchors.centerIn: parent - onClosed: { - destroy() + Connections { + target: profileModel + ignoreUnknownSignals: true + enabled: removeMnemonicAfterLogin + onInitialized: { + profileModel.mnemonic.remove() } } - } - Component { - id: communityProfilePopup + Connections { + target: profileModel.contacts + onContactRequestAdded: { + if (!appSettings.notifyOnNewRequests) { + return + } + const isContact = profileModel.contacts.isAdded(address) + //% "Contact request accepted" + systemTray.showMessage(isContact ? qsTrId("contact-request-accepted") : + //% "New contact request" + qsTrId("new-contact-request"), + //% "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)), + SystemTrayIcon.NoIcon, + Constants.notificationPopupTTL) + } + } - CommunityProfilePopup { + Component { + id: chooseBrowserPopupComponent + ChooseBrowserPopup { + onClosed: { + destroy() + } + } + } + + Component { + id: inviteFriendsToCommunityPopup + InviteFriendsToCommunityPopup { + anchors.centerIn: parent + onClosed: { + destroy() + } + } + } + + Component { id: communityProfilePopup - anchors.centerIn: parent - onClosed: { - destroy() + CommunityProfilePopup { + id: communityProfilePopup + anchors.centerIn: parent + + onClosed: { + destroy() + } } } - } - Component { - id: editCommunityPopup - CreateCommunityPopup { - anchors.centerIn: parent - isEdit: true - onClosed: { - destroy() + Component { + id: editCommunityPopup + CreateCommunityPopup { + anchors.centerIn: parent + isEdit: true + onClosed: { + destroy() + } } } - } - Component { - id: editChannelPopup - CreateChannelPopup { - anchors.centerIn: parent - isEdit: true - pinnedMessagesPopupComponent: chatLayoutContainer.chatColumn.pinnedMessagesPopupComponent - onClosed: { - destroy() + Component { + id: editChannelPopup + CreateChannelPopup { + anchors.centerIn: parent + isEdit: true + pinnedMessagesPopupComponent: chatLayoutContainer.chatColumn.pinnedMessagesPopupComponent + onClosed: { + destroy() + } } } - } - Component { - id: genericConfirmationDialog - ConfirmationDialog { - onClosed: { - destroy() + Component { + id: genericConfirmationDialog + ConfirmationDialog { + onClosed: { + destroy() + } } } - } - ToastMessage { - id: toastMessage - } - // Add SendModal here as it is used by the Wallet as well as the Browser - Loader { - id: sendModal - active: false - - function open() { - this.active = true - this.item.open() + ToastMessage { + id: toastMessage } - function closed() { - // this.sourceComponent = undefined // kill an opened instance - this.active = false - } - sourceComponent: SendModal { - onOpened: { - walletModel.gasView.getGasPricePredictions() + // Add SendModal here as it is used by the Wallet as well as the Browser + Loader { + id: sendModal + active: false + + function open() { + this.active = true + this.item.open() } - onClosed: { - sendModal.closed() + function closed() { + // this.sourceComponent = undefined // kill an opened instance + this.active = false + } + sourceComponent: SendModal { + onOpened: { + walletModel.gasView.getGasPricePredictions() + } + onClosed: { + sendModal.closed() + } } } - } - Action { - shortcut: "Ctrl+1" - onTriggered: changeAppSection(Constants.chat) - } - Action { - shortcut: "Ctrl+2" - onTriggered: changeAppSection(Constants.browser) - } - Action { - shortcut: "Ctrl+3" - onTriggered: changeAppSection(Constants.wallet) - } - Action { - shortcut: "Ctrl+4, Ctrl+," - onTriggered: changeAppSection(Constants.profile) - } - Action { - shortcut: "Ctrl+K" - onTriggered: { - if (channelPicker.opened) { + Action { + shortcut: "Ctrl+1" + onTriggered: changeAppSection(Constants.chat) + } + Action { + shortcut: "Ctrl+2" + onTriggered: changeAppSection(Constants.browser) + } + Action { + shortcut: "Ctrl+3" + onTriggered: changeAppSection(Constants.wallet) + } + Action { + shortcut: "Ctrl+4, Ctrl+," + onTriggered: changeAppSection(Constants.profile) + } + Action { + shortcut: "Ctrl+K" + onTriggered: { + if (channelPicker.opened) { + channelPicker.close() + } else { + channelPicker.open() + } + } + } + + Component { + id: statusIdenticonComponent + StatusIdenticon {} + } + + 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 statusIdenticonComponent.createObject(parent, { + width: channelPicker.imageWidth, + height: channelPicker.imageHeight, + chatName: modelData.name, + chatType: modelData.chatType, + identicon: modelData.identicon + }); + } + onClicked: function (index) { + appMain.changeAppSection(Constants.chat) + chatsModel.channelView.setActiveChannelByIndex(index) channelPicker.close() - } else { - channelPicker.open() } } } - - Component { - id: statusIdenticonComponent - StatusIdenticon {} - } - - 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 statusIdenticonComponent.createObject(parent, { - width: channelPicker.imageWidth, - height: channelPicker.imageHeight, - chatName: modelData.name, - chatType: modelData.chatType, - identicon: modelData.identicon - }); - } - onClicked: function (index) { - appMain.changeAppSection(Constants.chat) - chatsModel.channelView.setActiveChannelByIndex(index) - channelPicker.close() - } - } } - /*##^## Designer { D{i:0;formeditorZoom:1.75;height:770;width:1232} } -##^##*/ +##^##*/ \ No newline at end of file diff --git a/ui/shared/DownloadModal.qml b/ui/shared/DownloadModal.qml new file mode 100644 index 0000000000..d30c522622 --- /dev/null +++ b/ui/shared/DownloadModal.qml @@ -0,0 +1,68 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import "../imports" +import "../shared/status" +import "./" + +ModalPopup { + id: popup + + property bool newVersionAvailable: true + property string downloadURL: "https://github.com/status-im/status-desktop/releases/latest" + + height: 240 + width: 400 + title: newVersionAvailable ? + qsTr("New version available!") : + qsTr("No new version available") + + SVGImage { + visible: newVersionAvailable + id: imgExclamation + width: 13.33 + height: 13.33 + sourceSize.height: height * 2 + sourceSize.width: width * 2 + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.PreserveAspectFit + source: "../app/img/exclamation_outline.svg" + } + + + StyledText { + visible: newVersionAvailable + id: innerText + text: qsTr("Make sure you have your account password and seed phrase stored. Without them you can lock yourself out of your account and lose funds.") + font.pixelSize: 13 + color: Style.current.red + anchors.top: imgExclamation.bottom + anchors.topMargin: Style.current.halfPadding + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + visible: !newVersionAvailable + id: innerText2 + text: qsTr("You are up to date!") + font.pixelSize: 15 + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.WordWrap + } + + footer: StatusButton { + id: confirmButton + text: newVersionAvailable ? + qsTr("Download") : + qsTr("Ok") + anchors.right: parent.right + onClicked: newVersionAvailable ? appMain.openLink(downloadURL) : close() + } +} + + + diff --git a/ui/app/AppLayouts/Wallet/SeedPhraseBackupWarning.qml b/ui/shared/ModuleWarning.qml similarity index 83% rename from ui/app/AppLayouts/Wallet/SeedPhraseBackupWarning.qml rename to ui/shared/ModuleWarning.qml index 19689469d4..27a43df795 100644 --- a/ui/app/AppLayouts/Wallet/SeedPhraseBackupWarning.qml +++ b/ui/shared/ModuleWarning.qml @@ -2,16 +2,19 @@ import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 import QtGraphicalEffects 1.13 -import "../../../imports" -import "../../../shared" -import "../Profile/Sections" +import "../imports" import "." Rectangle { id: root - visible: !profileModel.mnemonic.isBackedUp height: visible ? 32 : 0 color: Style.current.red + + property string text: "" + property string btnText: "" + property int btnWidth: 58 + + property var onClick: function() {} Row { spacing: Style.current.halfPadding @@ -19,20 +22,19 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter StyledText { - //% "Back up your seed phrase" - text: qsTrId("back-up-your-seed-phrase") + text: root.text font.pixelSize: 13 anchors.verticalCenter: parent.verticalCenter color: Style.current.white } Button { - width: 58 + width: btnWidth height: 24 contentItem: Item { anchors.fill: parent Text { - text: "Back up" + text: btnText font.pixelSize: 13 font.weight: Font.Medium font.family: Style.current.fontRegular.name @@ -54,7 +56,7 @@ Rectangle { MouseArea { cursorShape: Qt.PointingHandCursor anchors.fill: parent - onClicked: backupSeedModal.open() + onClicked: root.onClick() } } } @@ -65,7 +67,7 @@ Rectangle { anchors.topMargin: 6 anchors.right: parent.right anchors.rightMargin: 18 - source: "../../img/close-white.svg" + source: "img/close-white.svg" height: 20 width: 20 } @@ -83,10 +85,4 @@ Rectangle { PropertyAnimation { target: root; property: "y"; to: -1 * root.height } } } - - - BackupSeedModal { - id: backupSeedModal - } - } diff --git a/ui/shared/img/close-white.svg b/ui/shared/img/close-white.svg new file mode 100644 index 0000000000..e9bd2f626a --- /dev/null +++ b/ui/shared/img/close-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/vendor/semver.nim b/vendor/semver.nim new file mode 160000 index 0000000000..3b7ace4870 --- /dev/null +++ b/vendor/semver.nim @@ -0,0 +1 @@ +Subproject commit 3b7ace48704236329363290c62ce43f4d35f684d