feat(@desktop/wallet): Added feature flag FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED for the send via personal chat feature
Also added logic in order to detect and highlight an address/ens name in the chat
This commit is contained in:
parent
baf3061fda
commit
5771a33eaa
|
@ -4,6 +4,7 @@ import os
|
|||
const DEFAULT_FLAG_DAPPS_ENABLED = false
|
||||
const DEFAULT_FLAG_SWAP_ENABLED = true
|
||||
const DEFAULT_FLAG_CONNECTOR_ENABLED* = false
|
||||
const DEFAULT_FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED = false
|
||||
|
||||
proc boolToEnv*(defaultValue: bool): string =
|
||||
return if defaultValue: "1" else: "0"
|
||||
|
@ -13,12 +14,14 @@ QtObject:
|
|||
dappsEnabled: bool
|
||||
swapEnabled: bool
|
||||
connectorEnabled: bool
|
||||
sendViaPersonalChatEnabled: bool
|
||||
|
||||
proc setup(self: FeatureFlags) =
|
||||
self.QObject.setup()
|
||||
self.dappsEnabled = getEnv("FLAG_DAPPS_ENABLED", boolToEnv(DEFAULT_FLAG_DAPPS_ENABLED)) != "0"
|
||||
self.swapEnabled = getEnv("FLAG_SWAP_ENABLED", boolToEnv(DEFAULT_FLAG_SWAP_ENABLED)) != "0"
|
||||
self.connectorEnabled = getEnv("FLAG_CONNECTOR_ENABLED", boolToEnv(DEFAULT_FLAG_CONNECTOR_ENABLED)) != "0"
|
||||
self.sendViaPersonalChatEnabled = getEnv("FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED", boolToEnv(DEFAULT_FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED)) != "0"
|
||||
|
||||
proc delete*(self: FeatureFlags) =
|
||||
self.QObject.delete()
|
||||
|
@ -44,3 +47,9 @@ QtObject:
|
|||
|
||||
QtProperty[bool] connectorEnabled:
|
||||
read = getConnectorEnabled
|
||||
|
||||
proc getSendViaPersonalChatEnabled*(self: FeatureFlags): bool {.slot.} =
|
||||
return self.sendViaPersonalChatEnabled
|
||||
|
||||
QtProperty[bool] sendViaPersonalChatEnabled:
|
||||
read = getSendViaPersonalChatEnabled
|
||||
|
|
|
@ -128,6 +128,30 @@ SplitView {
|
|||
outgoingStatus: StatusMessage.OutgoingStatus.Expired
|
||||
resendError: "can't send message on Tuesday"
|
||||
}
|
||||
ListElement {
|
||||
timestamp: 1667937930159
|
||||
senderId: "zqdeadbeef"
|
||||
senderDisplayName: "replicator.stateofus.eth"
|
||||
contentType: StatusMessage.ContentType.Text
|
||||
message: "Test message with a link https://github.com/. Hey annyah! 0x16437e05858c1a34f0ae63c9ca960d61a5583d5e
|
||||
this is my wallet address eth:opt:arb:0x16437e05858c1a34f0ae63c9ca960d61a5583d5e,
|
||||
0x75d5673fc25bb4993ea1218d9d415487c3656853"
|
||||
isContact: true
|
||||
isAReply: true
|
||||
trustIndicator: StatusContactVerificationIcons.TrustedType.None
|
||||
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
|
||||
}
|
||||
ListElement {
|
||||
timestamp: 1667937930159
|
||||
senderId: "zqdeadbeef"
|
||||
senderDisplayName: "replicator.stateofus.eth"
|
||||
contentType: StatusMessage.ContentType.Text
|
||||
message: "Ola!! qwerty.stateofus.eth hey this is my ens name"
|
||||
isContact: true
|
||||
isAReply: false
|
||||
trustIndicator: StatusContactVerificationIcons.TrustedType.None
|
||||
outgoingStatus: StatusMessage.OutgoingStatus.Delivered
|
||||
}
|
||||
}
|
||||
readonly property var colorHash: ListModel {
|
||||
ListElement { colorId: 13; segmentLength: 5 }
|
||||
|
@ -161,6 +185,7 @@ SplitView {
|
|||
isAReply: model.isAReply
|
||||
outgoingStatus: model.outgoingStatus
|
||||
resendError: model.outgoingStatus === StatusMessage.OutgoingStatus.Expired ? model.resendError : ""
|
||||
linkAddressAndEnsName: true
|
||||
|
||||
messageDetails {
|
||||
readonly property bool isEnsVerified: model.senderDisplayName.endsWith(".eth")
|
||||
|
@ -196,6 +221,7 @@ SplitView {
|
|||
onReplyProfileClicked: logs.logEvent("StatusMessage::replyProfileClicked")
|
||||
onReplyMessageClicked: logs.logEvent("StatusMessage::replyMessageClicked")
|
||||
onResendClicked: logs.logEvent("StatusMessage::resendClicked")
|
||||
onLinkActivated: logs.logEvent("StatusMessage::linkActivated" + link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import QtQuick 2.15
|
||||
import QtTest 1.15
|
||||
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 600
|
||||
height: 400
|
||||
|
||||
Component {
|
||||
id: componentUnderTest
|
||||
StatusMessage {
|
||||
anchors.fill: parent
|
||||
messageDetails {
|
||||
messageText: ""
|
||||
contentType: StatusMessage.ContentType.Text
|
||||
amISender: false
|
||||
sender.id: "zq123456790"
|
||||
sender.displayName: "Alice"
|
||||
sender.isContact: true
|
||||
sender.trustIndicator: StatusContactVerificationIcons.TrustedType.None
|
||||
sender.isEnsVerified: false
|
||||
sender.profileImage {
|
||||
name: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAiElEQVR4nOzXUQpAQBRGYWQvLNAyLJDV8C5qpiGnv/M9al5Ot27X0IUwhMYQGkNoDKGJCRlLH67bftx9X+ap/+P9VcxEDKExhKZ4a9Uq3TZviZmIITSG0DRvlqcbqVbrlouZiCE0htD4h0hjCI0hNN5aNIbQGKKPxEzEEBpDaAyhMYTmDAAA//+gYCErzmCpCQAAAABJRU5ErkJggg="
|
||||
colorId: 1
|
||||
colorHash: "#51D0F0"
|
||||
}
|
||||
}
|
||||
linkAddressAndEnsName: true
|
||||
outgoingStatus: StatusMessage.OutgoingStatus.Sending
|
||||
}
|
||||
}
|
||||
|
||||
property StatusMessage controlUnderTest: null
|
||||
|
||||
TestCase {
|
||||
name: "TokenSelectorView"
|
||||
when: windowShown
|
||||
|
||||
function init() {
|
||||
controlUnderTest = createTemporaryObject(componentUnderTest, root)
|
||||
}
|
||||
|
||||
|
||||
function test_different_address_formats_data() {
|
||||
return [
|
||||
{messageText: "0x1234567890abcdef1234567890abcdef12345678", validAddressEnsCount: 1}, // Valid ETH address
|
||||
{messageText: "0x1234567890abcdef1234567890abcdef12345678,
|
||||
0x16437e05858c1a34f0ae63c9ca960d61a5583d5e,
|
||||
0x75d5673fc25bb4993ea1218d9d415487c3656853", validAddressEnsCount: 3}, // Valid ETH address
|
||||
{messageText: "0xAbCdEf1234567890abcdef1234567890AbCdEf12", validAddressEnsCount: 1}, // Valid ETH address
|
||||
{messageText: "0x123", validAddressEnsCount: 0}, // Invalid ETH address (too short)
|
||||
{messageText: "1234567890abcdef1234567890abcdef12345678", validAddressEnsCount: 0}, // Invalid ETH address (no 0x)
|
||||
{messageText: "qwerty.stateofus.eth", validAddressEnsCount: 1}, // Valid ETH address
|
||||
{messageText: "alice.eth", validAddressEnsCount: 1}, // Valid ENS name
|
||||
{messageText: "bob.eth", validAddressEnsCount: 1}, // Valid ENS name
|
||||
{messageText: "sub.alice.eth", validAddressEnsCount: 1}, // Valid ENS name with subdomain
|
||||
{messageText: "bob.stateofus.eth", validAddressEnsCount: 1}, // Valid ENS name with subdomain
|
||||
{messageText: "ens.sub.sub.eth", validAddressEnsCount: 1}, // Valid ENS name with multiple subdomains
|
||||
{messageText: "example.com", validAddressEnsCount: 0}, // Invalid DNS-based ENS name
|
||||
{messageText: "another.example.xyz", validAddressEnsCount: 0}, // Invalid DNS-based ENS name
|
||||
{messageText: "my-site.io", validAddressEnsCount: 0}, // Invalid DNS-based ENS name
|
||||
{messageText: "invalid.ethaddress", validAddressEnsCount: 0}, // Invalid ENS-like name
|
||||
{messageText: "bob.eth.invalid", validAddressEnsCount: 0}, // Invalid ENS-like name (invalid TLD)
|
||||
{messageText: "My wallet is 0x1234567890abcdef1234567890abcdef12345678, and my ENS is alice.eth.", validAddressEnsCount: 2}, // Valid ETH and ENS in sentence
|
||||
{messageText: "You can find me at bob.eth or contact me via 0xAbCdEf1234567890abcdef1234567890AbCdEf12.", validAddressEnsCount: 2}, // Valid ETH and ENS in sentence
|
||||
{messageText: "Invalid address: 0x12345 and valid ENS: sub.alice.eth.", validAddressEnsCount: 1}, // Mixed case with valid and invalid
|
||||
{messageText: "Check 0x123GHIJKLMNOPQRSTUVWXYZ and visit example.com.", validAddressEnsCount: 0}, // Mixed case with valid DNS and invalid ETH
|
||||
{messageText: "0x1234567890abcdef1234567890abcdef12345678, qwerty.stateofus.eth, 0x16437e05858c1a34f0ae63c9ca960d61a5583d5e, 0x75d5673fc25bb4993ea1218d9d415487c3656853", validAddressEnsCount: 4}, // Valid ETH address
|
||||
]
|
||||
}
|
||||
|
||||
function test_different_address_formats(data) {
|
||||
verify(!!controlUnderTest)
|
||||
|
||||
controlUnderTest.messageDetails.messageText = data.messageText
|
||||
waitForRendering(controlUnderTest)
|
||||
|
||||
const statusTextMessage = findChild(controlUnderTest, "StatusMessage_textMessage")
|
||||
verify(!!statusTextMessage)
|
||||
|
||||
// Use regular expression to match all <a> tags in the text
|
||||
var linkMatches = statusTextMessage.textField.text.match(/<a\b[^>]*>(.*?)<\/a>/gi)
|
||||
var actualLinkCount = linkMatches ? linkMatches.length : 0
|
||||
|
||||
compare(actualLinkCount, data.validAddressEnsCount, "TextEdit should contain a link %1".arg(data.messageText))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,6 +75,7 @@ Control {
|
|||
property bool isInPinnedPopup
|
||||
property string highlightedLink: ""
|
||||
property string hoveredLink: ""
|
||||
property bool linkAddressAndEnsName
|
||||
|
||||
property StatusMessageDetails messageDetails: StatusMessageDetails {}
|
||||
property StatusMessageDetails replyDetails: StatusMessageDetails {}
|
||||
|
@ -288,6 +289,7 @@ Control {
|
|||
allowShowMore: !root.isInPinnedPopup
|
||||
textField.anchors.rightMargin: root.isInPinnedPopup ? /*Style.current.xlPadding*/ 32 : 0 // margin for the "Unpin" floating button
|
||||
highlightedLink: root.highlightedLink
|
||||
linkAddressAndEnsName: root.linkAddressAndEnsName
|
||||
onLinkActivated: {
|
||||
root.linkActivated(link);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ Item {
|
|||
readonly property alias hoveredLink: chatText.hoveredLink
|
||||
|
||||
property string highlightedLink: ""
|
||||
property bool linkAddressAndEnsName
|
||||
|
||||
property StatusMessageDetails messageDetails: StatusMessageDetails {}
|
||||
property bool isEdited: false
|
||||
|
@ -42,7 +43,7 @@ Item {
|
|||
if (root.messageDetails.contentType === StatusMessage.ContentType.Emoji && !root.isEdited)
|
||||
return Emoji.parse(root.messageDetails.messageText, Emoji.size.middle, Emoji.format.png);
|
||||
|
||||
let formattedMessage = Utils.linkifyAndXSS(root.messageDetails.messageText);
|
||||
let formattedMessage = Utils.linkifyAndXSS(root.messageDetails.messageText, root.linkAddressAndEnsName);
|
||||
|
||||
isQuote = (formattedMessage.startsWith("<blockquote>") && formattedMessage.endsWith("</blockquote>"));
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ QtObject {
|
|||
}
|
||||
}
|
||||
|
||||
function linkifyAndXSS(inputText) {
|
||||
function linkifyAndXSS(inputText, linkAddressAndEnsName = false) {
|
||||
//URLs starting with http://, https://, ftp:// or status-app://
|
||||
var replacePattern1 = /(\b(https?|ftp|status-app):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;\(\)]*[-A-Z0-9+&@#\/%=~_|])/gim;
|
||||
var replacedText = inputText.replace(replacePattern1, "<a href='$1'>$1</a>");
|
||||
|
@ -166,6 +166,18 @@ QtObject {
|
|||
var replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
|
||||
replacedText = replacedText.replace(replacePattern2, "$1<a href='http://$2'>$2</a>");
|
||||
|
||||
if (linkAddressAndEnsName) {
|
||||
// Wallet address
|
||||
var replacePatternWalletAddress = /(^|[^\/])(0x[a-fA-F0-9]{40})/gim;
|
||||
replacedText = replacedText.replace(replacePatternWalletAddress, "$1<a href='//send-via-personal-chat//$2'>$2</a>");
|
||||
|
||||
// Ens Name
|
||||
var replacePatternENS = /\b[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.eth\b(?!\.\w)/g;
|
||||
replacedText = replacedText.replace(replacePatternENS, function(match) {
|
||||
return "<a href='//send-via-personal-chat//" + match + "'>" + match + "</a>";
|
||||
});
|
||||
}
|
||||
|
||||
return XSS.filterXSS(replacedText)
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,8 @@ StackLayout {
|
|||
|
||||
property bool communitySettingsDisabled
|
||||
|
||||
property bool sendViaPersonalChatEnabled
|
||||
|
||||
property var emojiPopup
|
||||
property var stickersPopup
|
||||
signal profileButtonClicked()
|
||||
|
@ -158,6 +160,7 @@ StackLayout {
|
|||
root.sectionItemModel.memberRole === Constants.memberRole.admin ||
|
||||
root.sectionItemModel.memberRole === Constants.memberRole.tokenMaster
|
||||
hasViewOnlyPermissions: root.permissionsStore.viewOnlyPermissionsModel.count > 0
|
||||
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
|
||||
|
||||
hasUnrestrictedViewOnlyPermission: {
|
||||
viewOnlyUnrestrictedPermissionHelper.revision
|
||||
|
|
|
@ -56,6 +56,8 @@ Item {
|
|||
readonly property bool isUserAdded: !!root.contactDetails && root.contactDetails.isContactRequestSent
|
||||
property bool amISectionAdmin: false
|
||||
|
||||
property bool sendViaPersonalChatEnabled
|
||||
|
||||
signal openStickerPackPopup(string stickerPackId)
|
||||
|
||||
// This function is called once `1:1` or `group` chat is created.
|
||||
|
@ -238,6 +240,7 @@ Item {
|
|||
stickersPopup: root.stickersPopup
|
||||
stickersLoaded: root.stickersLoaded
|
||||
isBlocked: model.blocked
|
||||
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
|
||||
onOpenStickerPackPopup: {
|
||||
root.openStickerPackPopup(stickerPackId)
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ ColumnLayout {
|
|||
chatSectionModule: root.rootStore.chatCommunitySectionModule
|
||||
}
|
||||
|
||||
property bool sendViaPersonalChatEnabled
|
||||
|
||||
signal showReplyArea(messageId: string)
|
||||
signal forceInputFocus()
|
||||
|
||||
|
@ -90,6 +92,7 @@ ColumnLayout {
|
|||
isOneToOne: root.chatType === Constants.chatType.oneToOne
|
||||
isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage
|
||||
channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
|
||||
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
|
||||
onShowReplyArea: (messageId, senderId) => {
|
||||
root.showReplyArea(messageId)
|
||||
}
|
||||
|
|
|
@ -45,6 +45,8 @@ Item {
|
|||
property bool isChatBlocked: false
|
||||
property bool isOneToOne: false
|
||||
|
||||
property bool sendViaPersonalChatEnabled
|
||||
|
||||
signal openStickerPackPopup(string stickerPackId)
|
||||
signal showReplyArea(string messageId, string author)
|
||||
signal editModeChanged(bool editModeOn)
|
||||
|
@ -276,6 +278,8 @@ Item {
|
|||
|
||||
isChatBlocked: root.isChatBlocked
|
||||
|
||||
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
|
||||
|
||||
chatId: root.chatId
|
||||
messageId: model.id
|
||||
communityId: model.communityId
|
||||
|
|
|
@ -72,6 +72,8 @@ StatusSectionLayout {
|
|||
property var assetsModel
|
||||
property var collectiblesModel
|
||||
|
||||
property bool sendViaPersonalChatEnabled
|
||||
|
||||
readonly property bool contentLocked: {
|
||||
if (!rootStore.chatCommunitySectionModule.isCommunity()) {
|
||||
return false
|
||||
|
@ -228,6 +230,7 @@ StatusSectionLayout {
|
|||
viewAndPostHoldingsModel: root.viewAndPostPermissionsModel
|
||||
canPost: !root.rootStore.chatCommunitySectionModule.isCommunity() || root.canPost
|
||||
amISectionAdmin: root.amISectionAdmin
|
||||
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
|
||||
onOpenStickerPackPopup: {
|
||||
Global.openPopup(statusStickerPackClickPopup, {packId: stickerPackId, store: root.stickersPopup.store} )
|
||||
}
|
||||
|
|
|
@ -4,4 +4,5 @@ QtObject {
|
|||
property bool connectorEnabled
|
||||
property bool dappsEnabled
|
||||
property bool swapEnabled
|
||||
property bool sendViaPersonalChatEnabled
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ Item {
|
|||
connectorEnabled: featureFlags ? featureFlags.connectorEnabled : false
|
||||
dappsEnabled: featureFlags ? featureFlags.dappsEnabled : false
|
||||
swapEnabled: featureFlags ? featureFlags.swapEnabled : false
|
||||
sendViaPersonalChatEnabled: featureFlags ? featureFlags.sendViaPersonalChatEnabled : false
|
||||
}
|
||||
|
||||
required property bool isCentralizedMetricsEnabled
|
||||
|
@ -1352,6 +1353,7 @@ Item {
|
|||
currencyStore: appMain.currencyStore
|
||||
emojiPopup: statusEmojiPopup.item
|
||||
stickersPopup: statusStickersPopupLoader.item
|
||||
sendViaPersonalChatEnabled: featureFlagsStore.sendViaPersonalChatEnabled
|
||||
|
||||
onProfileButtonClicked: {
|
||||
Global.changeAppSectionBySectionType(Constants.appSection.profile);
|
||||
|
|
|
@ -131,6 +131,8 @@ Loader {
|
|||
|
||||
property bool hasMention: false
|
||||
|
||||
property bool sendViaPersonalChatEnabled
|
||||
|
||||
property bool stickersLoaded: false
|
||||
property string sticker
|
||||
property int stickerPack: -1
|
||||
|
@ -712,6 +714,7 @@ Loader {
|
|||
|
||||
disableEmojis: !d.addReactionAllowed
|
||||
hideMessage: d.hideMessage
|
||||
linkAddressAndEnsName: root.sendViaPersonalChatEnabled
|
||||
|
||||
overrideBackground: root.placeholderMessage
|
||||
profileClickable: !root.isDiscordMessage
|
||||
|
@ -728,6 +731,12 @@ Loader {
|
|||
}
|
||||
|
||||
onLinkActivated: {
|
||||
if (link.startsWith(Constants.sendViaChatPrefix)) {
|
||||
const addressOrEns = link.replace(Constants.sendViaChatPrefix, "");
|
||||
// TODO:: will be removed in the PRs to follow
|
||||
Global.displayToastMessage(qsTr("TODO:: Send Via 1-1"), addressOrEns, "", false, 0, "")
|
||||
return
|
||||
}
|
||||
if (link.startsWith('//')) {
|
||||
const pubkey = link.replace("//", "");
|
||||
Global.openProfilePopup(pubkey)
|
||||
|
|
|
@ -987,6 +987,7 @@ QtObject {
|
|||
readonly property string statusLinkPrefix: 'https://status.im/'
|
||||
readonly property string statusHelpLinkPrefix: `https://status.app/help/`
|
||||
readonly property string downloadLink: "https://status.im/get"
|
||||
readonly property string sendViaChatPrefix: '//send-via-personal-chat//'
|
||||
|
||||
readonly property int maxUploadFiles: 6
|
||||
readonly property double maxUploadFilesizeMB: 10
|
||||
|
|
Loading…
Reference in New Issue