diff --git a/resources/icons/Icon_Donate.svg b/resources/icons/Icon_Donate.svg new file mode 100644 index 0000000000000000000000000000000000000000..913daefdb94e8318a669aca684d2ff4df54617cc --- /dev/null +++ b/resources/icons/Icon_Donate.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="43" height="52.655" viewBox="0 0 43 52.655"> + <defs> + <clipPath id="clip-path"> + <rect id="Rectangle_268" data-name="Rectangle 268" width="38" height="24" transform="translate(-0.407 0.083)" fill="#fff" stroke="#707070" stroke-width="1"/> + </clipPath> + </defs> + <g id="Icon_Donate" transform="translate(-22 -189.345)"> + <rect id="Rectangle_267" data-name="Rectangle 267" width="43" height="10" rx="5" transform="translate(22 232)" fill="#9eb3c3"/> + <path id="Path_459" data-name="Path 459" d="M9.674,17.083,8.562,16.07C4.609,12.486,2,10.122,2,7.221A4.18,4.18,0,0,1,6.221,3,4.6,4.6,0,0,1,9.674,4.6,4.6,4.6,0,0,1,13.128,3a4.18,4.18,0,0,1,4.221,4.221c0,2.9-2.609,5.265-6.562,8.856Z" transform="translate(22.407 199.828)" fill="#ff0045" opacity="0.3"/> + <path id="Path_460" data-name="Path 460" d="M6.953,12.088l-.718-.654C3.684,9.122,2,7.6,2,5.724A2.7,2.7,0,0,1,4.724,3,2.966,2.966,0,0,1,6.953,4.035,2.966,2.966,0,0,1,9.182,3a2.7,2.7,0,0,1,2.724,2.724c0,1.872-1.684,3.4-4.235,5.716Z" transform="translate(45.571 186.345)" fill="#ff0045" opacity="0.16"/> + <g id="Mask_Group_38" data-name="Mask Group 38" transform="translate(24.407 213.918)" clip-path="url(#clip-path)"> + <path id="Path_270" data-name="Path 270" d="M12.649,22.542l-1.544-1.406C5.621,16.163,2,12.883,2,8.857A5.8,5.8,0,0,1,7.857,3a6.377,6.377,0,0,1,4.792,2.226A6.377,6.377,0,0,1,17.442,3,5.8,5.8,0,0,1,23.3,8.857c0,4.025-3.621,7.306-9.105,12.289Z" transform="translate(5.992 5.54)" fill="#ff0045"/> + </g> + </g> +</svg> diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h index 8b29766ecedbe5d2da4f495576d48cce5bad5b40..b28deddc04efe09df90e1da31a184cc66a8c75e9 100644 --- a/src/app/appsettingsmanager.h +++ b/src/app/appsettingsmanager.h @@ -62,8 +62,8 @@ extern const QString defaultDownloadPath; X(FlipSelf, true) \ X(ShowMardownOption, false) \ X(ChatViewEnterIsNewLine, false) \ - X(ShowSendOption, false) - + X(ShowSendOption, false) \ + X(DonateVisibleDate, "2999-02-01 05:00") /* * A class to expose settings keys in both c++ and QML. * Note: this is using a non-constructable class instead of a diff --git a/src/app/constant/JamiQmlUtils.qml b/src/app/constant/JamiQmlUtils.qml index 4eaa02f8775f70540c12bdbd97e935cf74766ec6..4b5aa62bd9571308f8f8d44596ea1993fe93b758 100644 --- a/src/app/constant/JamiQmlUtils.qml +++ b/src/app/constant/JamiQmlUtils.qml @@ -20,6 +20,7 @@ pragma Singleton import QtQuick import net.jami.Adapters 1.1 +import net.jami.Enums 1.1 Item { property string qmlFilePrefix: "file:/" @@ -69,4 +70,9 @@ Item { function clamp(val, min, max) { return Math.min(Math.max(val, min), max); } + + function isDonationBannerVisible() { + // The banner is visible if the current date is after the date set in the settings + return new Date() > new Date(Date.parse(UtilsAdapter.getAppValue(Settings.Key.DonateVisibleDate))); + } } diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 98f339d55a8372b7d2952518cdba3d55e7a61a6f..817ce6425f218fc2921ced6edcfc27565c8c0e94 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -835,4 +835,9 @@ Item { // Appearence property string theme: qsTr("Theme") property string zoomLevel: qsTr("Text zoom level") + + //Donation campaign + property string donation: qsTr("Donate") + property string donationText: qsTr("If you enjoy using Jami and believe in our mission, would you make a donation?") + property string notNow: qsTr("Not now") } diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml index 7a42dd6f59051bfaa0c0fd8f9b2db2e3d0c4f56f..b66d734549357f2f611100cc2da7c052a83ce702 100644 --- a/src/app/constant/JamiTheme.qml +++ b/src/app/constant/JamiTheme.qml @@ -657,6 +657,11 @@ Item { property color darkThemeCheckedColor: "#03B9E9" property color darkThemeBorderColor: "#03B9E9" + // Donation campaign + property color donationButtonTextColor: "#005699" + property color donationBackgroundColor: "#D5E4EF" + property string donationUrl: "https://jami.net/donate/" + function setTheme(dark) { darkTheme = dark; } diff --git a/src/app/mainview/components/DonationBanner.qml b/src/app/mainview/components/DonationBanner.qml new file mode 100644 index 0000000000000000000000000000000000000000..b7538878f917900f9e928809ea5ed8efbd9150ad --- /dev/null +++ b/src/app/mainview/components/DonationBanner.qml @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 +import net.jami.Enums 1.1 +import net.jami.Models 1.1 +import "../../commoncomponents" +import "../../settingsview/components" + +Rectangle { + id: donation + + property bool donationVisible: JamiQmlUtils.isDonationBannerVisible() + + width: parent.width - 30 + height: donationTextRect.height + 45 > donationIcon.height + 20 ? donationTextRect.height + 45 : donationIcon.height + 20 + radius: 5 + + color: JamiTheme.donationBackgroundColor + + GridLayout { + id: donationLayout + + anchors.fill: parent + columns: 3 + rows: 2 + rowSpacing: 0 + columnSpacing: 10 + + Rectangle { + id: donationIcon + + Layout.row: 0 + Layout.column: 0 + Layout.rowSpan: 2 + Layout.preferredHeight: 70 + Layout.preferredWidth: 45 + Layout.leftMargin: 10 + Layout.topMargin: 10 + Layout.bottomMargin: 15 + + color: JamiTheme.transparentColor + + Image { + id: donationImage + height: parent.height + width: 50 + anchors.centerIn: parent + source: JamiResources.icon_donate_svg + } + } + + Rectangle { + id: donationTextRect + + Layout.topMargin: 10 + Layout.row: 0 + Layout.column: 1 + Layout.columnSpan: 2 + Layout.preferredHeight: donationText.height + Layout.preferredWidth: parent.width - 74 + Layout.bottomMargin: 5 + color: JamiTheme.transparentColor + + Text { + id: donationText + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + width: parent.width + height: contentHeight + text: JamiStrings.donationText + wrapMode: Text.WordWrap + + font.pointSize: JamiTheme.textFontSize + } + } + + Rectangle { + id: notNowRect + + Layout.row: 1 + Layout.column: 1 + Layout.preferredHeight: 30 + Layout.preferredWidth: (parent.width - 55) / 2 + + color: JamiTheme.transparentColor + + Text { + id: notNowText + MouseArea { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + onClicked: { + // When the user clicks on "Not now", we set the donation date to 7 days from now (1 for the test) + // TODO reset to 7 days + UtilsAdapter.setAppValue(Settings.Key.DonateVisibleDate, new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000).toISOString().slice(0, 16).replace("T", " ")); + donation.donationVisible = Qt.binding(() => JamiQmlUtils.isDonationBannerVisible()); + } + } + text: JamiStrings.notNow + color: JamiTheme.donationButtonTextColor + anchors.top: parent.top + anchors.left: parent.left + font.pointSize: JamiTheme.textFontSize + } + } + + Rectangle { + id: donateRect + Layout.row: 1 + Layout.column: 2 + Layout.preferredHeight: 30 + Layout.preferredWidth: (parent.width - 50) / 2 + color: JamiTheme.transparentColor + + Text { + id: donateText + MouseArea { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + onClicked: { + Qt.openUrlExternally(JamiTheme.donationUrl); + } + } + text: JamiStrings.donation + font.pointSize: JamiTheme.textFontSize + color: JamiTheme.donationButtonTextColor + anchors.top: parent.top + anchors.left: parent.left + } + } + } +} diff --git a/src/app/mainview/components/SidePanel.qml b/src/app/mainview/components/SidePanel.qml index 170ade693d2e7e3167a0307375a5149131f4e44a..5ed1b5921122623c0076b1ec0b56210b48566798 100644 --- a/src/app/mainview/components/SidePanel.qml +++ b/src/app/mainview/components/SidePanel.qml @@ -16,21 +16,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - import QtQuick import QtQuick.Controls import QtQuick.Layouts - import net.jami.Adapters 1.1 import net.jami.Constants 1.1 import net.jami.Enums 1.1 import net.jami.Models 1.1 - import "../../commoncomponents" import "../../settingsview/components" SidePanelBase { id: root + objectName: "SidePanel" color: JamiTheme.backgroundColor @@ -39,7 +37,7 @@ SidePanelBase { target: LRCInstance function onCurrentAccountIdChanged() { - clearContactSearchBar() + clearContactSearchBar(); } } @@ -47,8 +45,8 @@ SidePanelBase { target: ConversationsAdapter function onConversationReady() { - selectTab(SidePanelTabBar.Conversations) - clearContactSearchBar() + selectTab(SidePanelTabBar.Conversations); + clearContactSearchBar(); } } @@ -56,46 +54,46 @@ SidePanelBase { target: ConversationsAdapter function onShowSearchStatus(status) { - searchStatusText.text = status + searchStatusText.text = status; } function onTextFilterChanged(text) { // In the swarm details, "Go to conversation" can // change the search bar. Be sure to be synced - contactSearchBar.textContent = text + contactSearchBar.textContent = text; } } function toggleCreateSwarmView() { if (!inNewSwarm) { - viewCoordinator.present("NewSwarmPage") - const newSwarmPage = viewCoordinator.getView("NewSwarmPage") + viewCoordinator.present("NewSwarmPage"); + const newSwarmPage = viewCoordinator.getView("NewSwarmPage"); newSwarmPage.removeMember.connect((convId, member) => { - removeMember(convId, member) - }) + removeMember(convId, member); + }); newSwarmPage.createSwarmClicked.connect((title, description, avatar) => { - var uris = [] - for (var idx in newSwarmPage.members) { - var uri = newSwarmPage.members[idx].uri - if (uris.indexOf(uri) === -1) { - uris.push(uri) + var uris = []; + for (var idx in newSwarmPage.members) { + var uri = newSwarmPage.members[idx].uri; + if (uris.indexOf(uri) === -1) { + uris.push(uri); + } } - } - let convuid = ConversationsAdapter.createSwarm(title, description, avatar, uris) - viewCoordinator.dismiss("NewSwarmPage") - LRCInstance.selectConversation(convuid) - }) + let convuid = ConversationsAdapter.createSwarm(title, description, avatar, uris); + viewCoordinator.dismiss("NewSwarmPage"); + LRCInstance.selectConversation(convuid); + }); } else { - viewCoordinator.dismiss("NewSwarmPage") + viewCoordinator.dismiss("NewSwarmPage"); } } function clearContactSearchBar() { - contactSearchBar.clearText() + contactSearchBar.clearText(); } function selectTab(tabIndex) { - sidePanelTabBar.selectTab(tabIndex) + sidePanelTabBar.selectTab(tabIndex); } property bool inNewSwarm: viewCoordinator.currentViewName === "NewSwarmPage" @@ -104,65 +102,64 @@ SidePanelBase { property var highlightedMembers: [] onHighlightedMembersChanged: { if (inNewSwarm) { - const newSwarmPage = viewCoordinator.getView("NewSwarmPage") - newSwarmPage.members = highlightedMembers + const newSwarmPage = viewCoordinator.getView("NewSwarmPage"); + newSwarmPage.members = highlightedMembers; } } function refreshHighlighted(convId, highlightedStatus) { - var newH = root.highlighted - var newHm = root.highlightedMembers - + var newH = root.highlighted; + var newHm = root.highlightedMembers; if (highlightedStatus) { - var item = ConversationsAdapter.getConvInfoMap(convId) - var added = false + var item = ConversationsAdapter.getConvInfoMap(convId); + var added = false; for (var idx in item.uris) { - var uri = item.uris[idx] - if (!Array.from(newHm).find(r => r.uri === uri) && - uri !== CurrentAccount.uri) { - newHm.push({"uri": uri, "convId": convId}) - added = true + var uri = item.uris[idx]; + if (!Array.from(newHm).find(r => r.uri === uri) && uri !== CurrentAccount.uri) { + newHm.push({ + "uri": uri, + "convId": convId + }); + added = true; } } if (!added) - return false + return false; } else { - newH = Array.from(newH).filter(r => r !== convId) - newHm = Array.from(newHm).filter(r => r.convId !== convId) + newH = Array.from(newH).filter(r => r !== convId); + newHm = Array.from(newHm).filter(r => r.convId !== convId); } - - newH.push(convId) - root.highlighted = newH - root.highlightedMembers = newHm - ConversationsAdapter.ignoreFiltering(root.highlighted) - return true + newH.push(convId); + root.highlighted = newH; + root.highlightedMembers = newHm; + ConversationsAdapter.ignoreFiltering(root.highlighted); + return true; } function clearHighlighted() { - root.highlighted = [] - root.highlightedMembers = [] + root.highlighted = []; + root.highlightedMembers = []; } function removeMember(convId, member) { - var refreshHighlighted = true - var newHm = [] + var refreshHighlighted = true; + var newHm = []; for (var hm in root.highlightedMembers) { - var m = root.highlightedMembers[hm] + var m = root.highlightedMembers[hm]; if (m.convId === convId && m.uri === member) { continue; } else if (m.convId === convId) { - refreshHighlighted = false + refreshHighlighted = false; } - newHm.push(m) + newHm.push(m); } - root.highlightedMembers = newHm - + root.highlightedMembers = newHm; if (refreshHighlighted) { // Remove highlighted status if necessary for (var d in swarmCurrentConversationList.contentItem.children) { - var delegate = swarmCurrentConversationList.contentItem.children[d] + var delegate = swarmCurrentConversationList.contentItem.children[d]; if (delegate.convId === convId) - delegate.highlighted = false + delegate.highlighted = false; } } } @@ -176,11 +173,16 @@ SidePanelBase { color: JamiTheme.backgroundColor } - header: AccountComboBox {} + header: AccountComboBox { + } Item { anchors.fill: parent + onVisibleChanged: { + donation.donationVisible = Qt.binding(() => JamiQmlUtils.isDonationBannerVisible()); + } + RowLayout { id: titleBar @@ -240,7 +242,7 @@ SidePanelBase { sequence: "Ctrl+F" context: Qt.ApplicationShortcut onActivated: { - contactSearchBar.forceActiveFocus() + contactSearchBar.forceActiveFocus(); } } @@ -250,20 +252,18 @@ SidePanelBase { Layout.fillHeight: true Layout.fillWidth: true - onSearchBarTextChanged: function(text){ + onSearchBarTextChanged: function (text) { // not calling positionViewAtBeginning will cause // sort animation visual bugs - conversationListView.positionViewAtBeginning() - ConversationsAdapter.ignoreFiltering(root.highlighted) - ConversationsAdapter.setFilter(text) + conversationListView.positionViewAtBeginning(); + ConversationsAdapter.ignoreFiltering(root.highlighted); + ConversationsAdapter.setFilter(text); } onReturnPressedWhileSearching: { - var listView = searchResultsListView.count ? - searchResultsListView : - conversationListView + var listView = searchResultsListView.count ? searchResultsListView : conversationListView; if (listView.count) - listView.model.select(0) + listView.model.select(0); } } @@ -291,8 +291,7 @@ SidePanelBase { SidePanelTabBar { id: sidePanelTabBar - visible: ConversationsAdapter.pendingRequestCount && - !contactSearchBar.textContent && smartListLayout.visible + visible: ConversationsAdapter.pendingRequestCount && !contactSearchBar.textContent && smartListLayout.visible anchors.top: startBar.bottom anchors.topMargin: visible ? 10 : 0 width: page.width @@ -311,7 +310,6 @@ SidePanelBase { height: visible ? 42 : 0 color: JamiTheme.backgroundColor - Text { id: searchStatusText @@ -326,13 +324,22 @@ SidePanelBase { } } + DonationBanner { + id: donation + anchors.horizontalCenter: parent.horizontalCenter + anchors.leftMargin: 15 + anchors.rightMargin: 15 + anchors.top: sidePanelTabBar.bottom + anchors.topMargin: 10 + visible: donation.donationVisible + } + ColumnLayout { id: smartListLayout width: parent.width - anchors.top: searchStatusRect.bottom - anchors.topMargin: (sidePanelTabBar.visible || - searchStatusRect.visible) ? 0 : 12 + anchors.top: donation.donationVisible ? donation.bottom : sidePanelTabBar.bottom + anchors.topMargin: (sidePanelTabBar.visible || searchStatusRect.visible) ? 0 : 12 anchors.bottom: parent.bottom spacing: 4 @@ -350,14 +357,14 @@ SidePanelBase { Layout.fillWidth: true Layout.preferredHeight: visible ? contentHeight : 0 Layout.maximumHeight: { - var otherContentHeight = conversationListView.contentHeight + 16 + var otherContentHeight = conversationListView.contentHeight + 16; if (conversationListView.visible) if (otherContentHeight < parent.height / 2) - return parent.height - otherContentHeight + return parent.height - otherContentHeight; else - return parent.height / 2 + return parent.height / 2; else - return parent.height + return parent.height; } model: SearchResultsListModel @@ -385,9 +392,8 @@ SidePanelBase { visible: inNewSwarm width: parent.width - anchors.top: searchStatusRect.bottom - anchors.topMargin: (sidePanelTabBar.visible || - searchStatusRect.visible) ? 0 : 12 + anchors.top: donation.donationVisible ? donation.bottom : sidePanelTabBar.bottom + anchors.topMargin: (sidePanelTabBar.visible || searchStatusRect.visible) ? 0 : 12 anchors.bottom: parent.bottom spacing: 4 @@ -404,8 +410,8 @@ SidePanelBase { onVisibleChanged: { if (!swarmCurrentConversationList.visible) { - highlighted = false - root.clearHighlighted() + highlighted = false; + root.clearHighlighted(); } } @@ -414,26 +420,26 @@ SidePanelBase { // destroyed from the memory. So, re-add the highlighted // status if necessary if (Array.from(root.highlighted).find(r => r === UID)) { - highlighted = true + highlighted = true; } } onHighlightedChanged: function onHighlightedChanged() { if (highlighted && Array.from(root.highlighted).find(r => r === UID)) { // Due to scrolling destruction/reconstruction - return + return; } - var currentHighlighted = root.highlighted + var currentHighlighted = root.highlighted; if (!root.refreshHighlighted(UID, highlighted)) { - highlighted = false - return + highlighted = false; + return; } if (highlighted) { - root.highlighted.push(UID) + root.highlighted.push(UID); } else { - root.highlighted = Array.from(root.highlighted).filter(r => r !== UID) + root.highlighted = Array.from(root.highlighted).filter(r => r !== UID); } - root.clearContactSearchBar() + root.clearContactSearchBar(); } } currentIndex: model.currentFilteredRow @@ -448,7 +454,9 @@ SidePanelBase { interval: 750 running: isSharingPosition || isReceivingPosition repeat: true - onTriggered: {showIconArrow = !showIconArrow} + onTriggered: { + showIconArrow = !showIconArrow; + } } } }