import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 import QtGraphicalEffects 1.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 shared.controls 1.0 import shared.stores 1.0 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import "../stores" import "../controls" import "../popups" import "../panels" SettingsContentBase { id: root property MessagingStore messagingStore property AdvancedStore advancedStore property ContactsStore contactsStore ColumnLayout { id: generalColumn spacing: 2 * Constants.settingsSection.itemSpacing width: root.contentWidth function switchOffPreviewableSites() { //update all models localAccountSensitiveSettings.displayChatImages = false for (let i = 0; i < previewableSites.count; i++) { let item = previewableSites.get(i) RootStore.updateWhitelistedUnfurlingSites(item.address, false) } } function buildPreviewablesSitesJSON() { let whitelistAsString = root.messagingStore.getLinkPreviewWhitelist() if(whitelistAsString == "") return if (!localAccountSensitiveSettings.whitelistedUnfurlingSites) { localAccountSensitiveSettings.whitelistedUnfurlingSites = {} } let anyWhitelisted = false let whitelist = JSON.parse(whitelistAsString) whitelist.forEach(entry => { entry.isWhitelisted = !!localAccountSensitiveSettings.whitelistedUnfurlingSites[entry.address] if(entry.isWhitelisted) anyWhitelisted = true }) return [anyWhitelisted, whitelist] } function populatePreviewableSites() { const [anyWhitelisted, whitelist] = buildPreviewablesSitesJSON() previewableSites.populateModel(whitelist) previewableSites.anyWhitelisted = anyWhitelisted } ButtonGroup { id: showProfilePictureToGroup } ButtonGroup { id: seeProfilePicturesFromGroup } ButtonGroup { id: browserGroup } StatusListItem { id: allowNewContactRequest Layout.fillWidth: true implicitHeight: 64 title: qsTr("Allow new contact requests") components: [ StatusSwitch { id: switch3 checked: !root.messagingStore.privacyModule.messagesFromContactsOnly onCheckedChanged: { // messagesFromContactsOnly needs to be accessed from the module (view), // because otherwise doing `messagesFromContactsOnly = value` only changes the bool property on QML if (root.messagingStore.privacyModule.messagesFromContactsOnly === checked) { root.messagingStore.privacyModule.messagesFromContactsOnly = !checked } } } ] onClicked: { switch3.checked = !switch3.checked } } // Open Message Links With StatusBaseText { Layout.topMargin: Constants.settingsSection.itemSpacing Layout.fillWidth: true Layout.leftMargin: Style.current.padding Layout.rightMargin: Style.current.padding text: qsTr("Open Message Links With") font.pixelSize: 15 color: Theme.palette.directColor1 } SettingsRadioButton { Layout.fillWidth: true Layout.leftMargin: Style.current.padding Layout.rightMargin: Style.current.padding label: qsTr("Status Browser") group: browserGroup checked: localAccountSensitiveSettings.openLinksInStatus onClicked: { localAccountSensitiveSettings.openLinksInStatus = true } } SettingsRadioButton { Layout.topMargin: Constants.settingsSection.itemSpacing / 2 Layout.fillWidth: true Layout.leftMargin: Style.current.padding Layout.rightMargin: Style.current.padding label: qsTr("System Default Browser") group: browserGroup checked: !localAccountSensitiveSettings.openLinksInStatus onClicked: { localAccountSensitiveSettings.openLinksInStatus = false } } Separator { id: separator1 Layout.topMargin: Constants.settingsSection.itemSpacing Layout.fillWidth: true } // CONTACTS SECTION StatusContactRequestsIndicatorListItem { objectName: "MessagingView_ContactsListItem_btn" Layout.fillWidth: true title: qsTr("Contacts, Requests, and Blocked Users") requestsCount: root.contactsStore.receivedContactRequestsModel.count onClicked: Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.contacts) } Separator { id: separator2 Layout.fillWidth: true } // MESSAGE LINK PREVIEWS StatusListItem { Layout.fillWidth: true objectName: "displayMessageLinkPreviewsItem" title: qsTr("Display Message Link Previews") implicitHeight: 64 components: [ StatusSwitch { id: showMessageLinksSwitch objectName: "MessagingView_showMessageLinksSwitch" checked: previewableSites.anyWhitelisted || localAccountSensitiveSettings.displayChatImages onToggled: { if (!checked) { generalColumn.switchOffPreviewableSites() } } } ] onClicked: { showMessageLinksSwitch.toggle() if (!showMessageLinksSwitch.checked) { generalColumn.switchOffPreviewableSites() } } } Component.onCompleted: { populatePreviewableSites() } StatusSectionHeadline { id: labelWebsites visible: showMessageLinksSwitch.checked Layout.fillWidth: true Layout.leftMargin: Style.current.padding Layout.rightMargin: Style.current.padding text: qsTr("Fine tune which sites to allow link previews") } Column { id: siteColumn visible: showMessageLinksSwitch.checked Layout.fillWidth: true ListModel { id: previewableSites function populateModel(jsonModel) { // add/update rows Object.entries(jsonModel) .forEach(([index, newRow]) => { var existingRow = previewableSites.get(index) let isRowIdentical = existingRow != undefined && Object.entries(newRow) .every(([key, value]) => value == existingRow[key]) if(!isRowIdentical) { previewableSites.set(index, newRow) } }) // remove rows that are not in the new model if(previewableSites.count > jsonModel.length) { let rowsToDelete = previewableSites.count - jsonModel.length previewableSites.remove(jsonModel.length - 1, rowsToDelete) } } property bool anyWhitelisted: false } Connections { target: Global function onSettingsLoaded() { generalColumn.populatePreviewableSites() } } Connections { target: localAccountSensitiveSettings function onWhitelistedUnfurlingSitesChanged() { generalColumn.populatePreviewableSites() } } // Manually add switch for the image unfurling StatusListItem { objectName: "imageUnfurlingItem" width: parent.width implicitHeight: 64 title: qsTr("Image unfurling") subTitle: qsTr("All images (links that contain an image extension) will be downloaded and displayed") // TODO find a better icon for this asset.name: Style.svg('globe') asset.isImage: true components: [ StatusSwitch { id: imageSwitch checked: localAccountSensitiveSettings.displayChatImages onToggled: { localAccountSensitiveSettings.displayChatImages = !localAccountSensitiveSettings.displayChatImages } } ] onClicked: { localAccountSensitiveSettings.displayChatImages = !localAccountSensitiveSettings.displayChatImages } } Repeater { id: sitesListView model: previewableSites delegate: Component { StatusListItem { objectName: "MessagingView_sitesListView_StatusListItem_" + model.title.replace(/ /g, "_").toLowerCase() width: parent.width implicitHeight: 64 title: model.title subTitle: model.address asset.name: { let filename; switch (model.title.toLowerCase()) { case "youtube": case "youtube shortener": filename = "youtube"; break; case "github": filename = "github"; break; case "medium": filename = "medium"; break; case "tenor gifs subdomain": filename = "tenor"; break; case "giphy gifs": case "giphy gifs shortener": case "giphy gifs subdomain": filename = "giphy"; break; case "github": filename = "github"; break; case "status": filename = "status"; break; // TODO get a good default icon default: filename = "../globe" } return Style.svg(`linkPreviewThumbnails/${filename}`) } asset.isImage: true components: [ StatusSwitch { id: siteSwitch checked: !!model.isWhitelisted onToggled: { RootStore.updateWhitelistedUnfurlingSites(model.address, checked) } } ] onClicked: { RootStore.updateWhitelistedUnfurlingSites(model.address, !model.isWhitelisted) } } } } } // Site Column Separator { id: separator3 visible: siteColumn.visible Layout.fillWidth: true } // SYNC WAKU SECTION StatusSectionHeadline { Layout.fillWidth: true Layout.leftMargin: Style.current.padding Layout.rightMargin: Style.current.padding text: qsTr("Message syncing") } StatusListItem { Layout.fillWidth: true title: qsTr("History nodes") label: root.messagingStore.getMailserverNameForNodeAddress(root.messagingStore.activeMailserver) components: [ StatusIcon { icon: "chevron-down" rotation: 270 color: Theme.palette.baseColor1 } ] onClicked: Global.openPopup(wakuStoreModalComponent) } Component { id: wakuStoreModalComponent WakuStoreModal { messagingStore: root.messagingStore advancedStore: root.advancedStore } } StatusListItem { Layout.fillWidth: true title: qsTr("Waku Nodes") visible: root.advancedStore.isWakuV2 components: [ StatusIcon { icon: "chevron-down" rotation: 270 color: Theme.palette.baseColor1 } ] onClicked: Global.openPopup(wakuNodesModalComponent) } Component { id: wakuNodesModalComponent WakuNodesModal { messagingStore: root.messagingStore advancedStore: root.advancedStore } } StatusSectionHeadline { Layout.fillWidth: true Layout.leftMargin: Style.current.padding Layout.rightMargin: Style.current.padding text: qsTr("For security reasons, private chat history won't be synced.") } } }