diff --git a/src/app/modules/main/activity_center/module.nim b/src/app/modules/main/activity_center/module.nim index bc566d85a23..8834e2777e2 100644 --- a/src/app/modules/main/activity_center/module.nim +++ b/src/app/modules/main/activity_center/module.nim @@ -75,7 +75,7 @@ proc createMessageItemFromDto(self: Module, message: MessageDto, chatDetails: Ch chatDetails.communityId, # we don't received community id via `activityCenterNotifications` api call message.responseTo, message.`from`, - contactDetails.displayName, + contactDetails.details.displayName, contactDetails.details.localNickname, contactDetails.icon, contactDetails.isCurrentUser, @@ -93,7 +93,8 @@ proc createMessageItemFromDto(self: Module, message: MessageDto, chatDetails: Ch message.links, newTransactionParametersItem("","","","","","",-1,""), message.mentionedUsersPks, - contactDetails.details.trustStatus + contactDetails.details.trustStatus, + contactDetails.details.ensVerified )) method convertToItems*( diff --git a/src/app/modules/main/chat_section/chat_content/messages/module.nim b/src/app/modules/main/chat_section/chat_content/messages/module.nim index 2e15916cb7d..426ffbbc891 100644 --- a/src/app/modules/main/chat_section/chat_content/messages/module.nim +++ b/src/app/modules/main/chat_section/chat_content/messages/module.nim @@ -91,10 +91,11 @@ proc createFetchMoreMessagesItem(self: Module): Item = messageType = -1, sticker = "", stickerPack = -1, - @[], - newTransactionParametersItem("","","","","","",-1,""), - @[], - TrustStatus.Unknown + links = @[], + transactionParameters = newTransactionParametersItem("","","","","","",-1,""), + mentionedUsersPks = @[], + senderTrustStatus = TrustStatus.Unknown, + senderEnsVerified = false ) proc createChatIdentifierItem(self: Module): Item = @@ -128,10 +129,11 @@ proc createChatIdentifierItem(self: Module): Item = messageType = -1, sticker = "", stickerPack = -1, - @[], - newTransactionParametersItem("","","","","","",-1,""), - @[], - TrustStatus.Unknown + links = @[], + transactionParameters = newTransactionParametersItem("","","","","","",-1,""), + mentionedUsersPks = @[], + senderTrustStatus = TrustStatus.Unknown, + senderEnsVerified = false ) proc checkIfMessageLoadedAndScrollToItIfItIs(self: Module): bool = @@ -177,7 +179,7 @@ method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: se m.communityId, m.responseTo, m.`from`, - sender.displayName, + sender.details.displayName, sender.details.localNickname, sender.icon, isCurrentUser, @@ -203,6 +205,7 @@ method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: se m.transactionParameters.signature), m.mentionedUsersPks(), sender.details.trustStatus, + sender.details.ensVerified ) for r in reactions: @@ -265,7 +268,7 @@ method messageAdded*(self: Module, message: MessageDto) = message.communityId, message.responseTo, message.`from`, - sender.displayName, + sender.details.displayName, sender.details.localNickname, sender.icon, isCurrentUser, @@ -291,6 +294,7 @@ method messageAdded*(self: Module, message: MessageDto) = message.transactionParameters.signature), message.mentionedUsersPks, sender.details.trustStatus, + sender.details.ensVerified ) self.view.model().insertItemBasedOnTimestamp(item) @@ -399,7 +403,7 @@ method updateContactDetails*(self: Module, contactId: string) = let updatedContact = self.controller.getContactDetails(contactId) for item in self.view.model().modelContactUpdateIterator(contactId): if(item.senderId == contactId): - item.senderDisplayName = updatedContact.displayName + item.senderDisplayName = updatedContact.details.displayName item.senderLocalName = updatedContact.details.localNickname item.senderIcon = updatedContact.icon item.senderIsAdded = updatedContact.details.added diff --git a/src/app/modules/main/chat_section/chat_content/module.nim b/src/app/modules/main/chat_section/chat_content/module.nim index 25bc6265361..14230c4cd13 100644 --- a/src/app/modules/main/chat_section/chat_content/module.nim +++ b/src/app/modules/main/chat_section/chat_content/module.nim @@ -167,7 +167,7 @@ proc buildPinnedMessageItem(self: Module, messageId: string, actionInitiatedBy: m.communityId, m.responseTo, m.`from`, - contactDetails.displayName, + contactDetails.details.displayName, contactDetails.details.localNickname, contactDetails.icon, isCurrentUser, @@ -193,6 +193,7 @@ proc buildPinnedMessageItem(self: Module, messageId: string, actionInitiatedBy: m.transactionParameters.signature), m.mentionedUsersPks, contactDetails.details.trustStatus, + contactDetails.details.ensVerified ) item.pinned = true item.pinnedBy = actionInitiatedBy @@ -318,8 +319,9 @@ method onContactDetailsUpdated*(self: Module, contactId: string) = let updatedContact = self.controller.getContactDetails(contactId) for item in self.view.pinnedModel().modelContactUpdateIterator(contactId): if(item.senderId == contactId): - item.senderDisplayName = updatedContact.displayName + item.senderDisplayName = updatedContact.details.displayName item.senderLocalName = updatedContact.details.localNickname + item.senderEnsVerified = updatedContact.details.ensVerified item.senderIcon = updatedContact.icon item.senderTrustStatus = updatedContact.details.trustStatus if(item.messageContainsMentions): @@ -329,7 +331,7 @@ method onContactDetailsUpdated*(self: Module, contactId: string) = item.messageContainsMentions = m.containsContactMentions() if(self.controller.getMyChatId() == contactId): - self.view.updateChatDetailsNameAndIcon(updatedContact.displayName, updatedContact.icon) + self.view.updateChatDetailsNameAndIcon(updatedContact.details.displayName, updatedContact.icon) self.view.updateTrustStatus(updatedContact.details.trustStatus == TrustStatus.Untrustworthy) method onNotificationsUpdated*(self: Module, hasUnreadMessages: bool, notificationCount: int) = diff --git a/src/app/modules/main/chat_section/chat_content/users/module.nim b/src/app/modules/main/chat_section/chat_content/users/module.nim index dacf912d5c9..bdd666cc905 100644 --- a/src/app/modules/main/chat_section/chat_content/users/module.nim +++ b/src/app/modules/main/chat_section/chat_content/users/module.nim @@ -75,7 +75,7 @@ method onNewMessagesLoaded*(self: Module, messages: seq[MessageDto]) = let status = toOnlineStatus(statusUpdateDto.statusType) self.view.model().addItem(initMemberItem( pubKey = m.`from`, - displayName = contactDetails.displayName, + displayName = contactDetails.details.displayName, ensName = contactDetails.details.name, # is it correct? localNickname = contactDetails.details.localNickname, alias = contactDetails.details.alias, @@ -132,7 +132,7 @@ method addChatMember*(self: Module, member: ChatMember) = let isMe = member.id == singletonInstance.userProfile.getPubKey() let contactDetails = self.controller.getContactDetails(member.id) var status = OnlineStatus.Online - var displayName = contactDetails.displayName + var displayName = contactDetails.details.displayName if (isMe): displayName = displayName & " (You)" let currentUserStatus = intToEnum(singletonInstance.userProfile.getCurrentUserStatus(), StatusType.Unknown) @@ -143,7 +143,7 @@ method addChatMember*(self: Module, member: ChatMember) = self.view.model().addItem(initMemberItem( pubKey = member.id, - displayName = displayName, + displayName = contactDetails.details.displayName, ensName = contactDetails.details.name, localNickname = contactDetails.details.localNickname, alias = contactDetails.details.alias, @@ -181,7 +181,7 @@ method onChatMemberUpdated*(self: Module, publicKey: string, admin: bool, joined let contactDetails = self.controller.getContactDetails(publicKey) self.view.model().updateItem( pubKey = publicKey, - displayName = contactDetails.displayName, + displayName = contactDetails.details.displayName, ensName = contactDetails.details.name, localNickname = contactDetails.details.localNickname, alias = contactDetails.details.alias, diff --git a/src/app/modules/main/profile_section/notifications/module.nim b/src/app/modules/main/profile_section/notifications/module.nim index 34a75246eb8..7ddaee7338f 100644 --- a/src/app/modules/main/profile_section/notifications/module.nim +++ b/src/app/modules/main/profile_section/notifications/module.nim @@ -65,7 +65,7 @@ proc createChatItem(self: Module, chatDto: ChatDto): Item = var itemType = item.Type.GroupChat if(chatDto.chatType == ChatType.OneToOne): let contactDetails = self.controller.getContactDetails(chatDto.id) - chatName = contactDetails.displayName + chatName = contactDetails.details.displayName chatImage = contactDetails.icon itemType = item.Type.OneToOneChat diff --git a/src/app/modules/shared_models/message_item.nim b/src/app/modules/shared_models/message_item.nim index a6ada2e8008..fcbef8d0c9b 100644 --- a/src/app/modules/shared_models/message_item.nim +++ b/src/app/modules/shared_models/message_item.nim @@ -37,6 +37,7 @@ type transactionParameters: TransactionParametersItem mentionedUsersPks: seq[string] senderTrustStatus: TrustStatus + senderEnsVerified: bool proc initItem*( id, @@ -61,7 +62,8 @@ proc initItem*( links: seq[string], transactionParameters: TransactionParametersItem, mentionedUsersPks: seq[string], - senderTrustStatus: TrustStatus + senderTrustStatus: TrustStatus, + senderEnsVerified: bool ): Item = result = Item() result.id = id @@ -93,6 +95,7 @@ proc initItem*( result.gapFrom = 0 result.gapTo = 0 result.senderTrustStatus = senderTrustStatus + result.senderEnsVerified = senderEnsVerified proc `$`*(self: Item): string = result = fmt"""Item( @@ -120,6 +123,7 @@ proc `$`*(self: Item): string = transactionParameters:{$self.transactionParameters}, mentionedUsersPks:{$self.mentionedUsersPks}, senderTrustStatus:{$self.senderTrustStatus}, + senderEnsVerified: {self.senderEnsVerified}, )""" proc id*(self: Item): string {.inline.} = @@ -167,6 +171,12 @@ proc senderTrustStatus*(self: Item): TrustStatus {.inline.} = proc `senderTrustStatus=`*(self: Item, value: TrustStatus) {.inline.} = self.senderTrustStatus = value +proc senderEnsVerified*(self: Item): bool {.inline.} = + self.senderEnsVerified + +proc `senderEnsVerified=`*(self: Item, value: bool) {.inline.} = + self.senderEnsVerified = value + proc outgoingStatus*(self: Item): string {.inline.} = self.outgoingStatus @@ -274,7 +284,8 @@ proc toJsonNode*(self: Item): JsonNode = "editMode": self.editMode, "isEdited": self.isEdited, "links": self.links, - "mentionedUsersPks": self.mentionedUsersPks + "mentionedUsersPks": self.mentionedUsersPks, + "senderEnsVerified": self.senderEnsVerified } proc editMode*(self: Item): bool {.inline.} = diff --git a/src/app/modules/shared_models/message_item_qobject.nim b/src/app/modules/shared_models/message_item_qobject.nim index cda1eb89d0c..f9e68db7006 100644 --- a/src/app/modules/shared_models/message_item_qobject.nim +++ b/src/app/modules/shared_models/message_item_qobject.nim @@ -49,6 +49,10 @@ QtObject: QtProperty[string] senderLocalName: read = senderLocalName + proc senderEnsVerified*(self: MessageItem): bool {.slot.} = result = ?.self.messageItem.senderEnsVerified + QtProperty[bool] senderEnsVerified: + read = senderEnsVerified + proc amISender*(self: MessageItem): bool {.slot.} = result = ?.self.messageItem.amISender QtProperty[bool] amISender: read = amISender diff --git a/src/app/modules/shared_models/message_model.nim b/src/app/modules/shared_models/message_model.nim index 3a00f7d6f26..4903847deff 100644 --- a/src/app/modules/shared_models/message_model.nim +++ b/src/app/modules/shared_models/message_model.nim @@ -37,6 +37,7 @@ type TransactionParameters MentionedUsersPks SenderTrustStatus + SenderEnsVerified QtObject: type @@ -70,7 +71,7 @@ QtObject: proc countChanged(self: Model) {.signal.} proc getCount(self: Model): int {.slot.} = self.items.len - QtProperty[int] count: + QtProperty[int]count: read = getCount notify = countChanged @@ -110,7 +111,8 @@ QtObject: ModelRole.Links.int: "links", ModelRole.TransactionParameters.int: "transactionParameters", ModelRole.MentionedUsersPks.int: "mentionedUsersPks", - ModelRole.SenderTrustStatus.int: "senderTrustStatus" + ModelRole.SenderTrustStatus.int: "senderTrustStatus", + ModelRole.SenderEnsVerified.int: "senderEnsVerified" }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -197,6 +199,8 @@ QtObject: })) of ModelRole.MentionedUsersPks: result = newQVariant(item.mentionedUsersPks.join(" ")) + of ModelRole.SenderEnsVerified: + result = newQVariant(item.senderEnsVerified) proc updateItemAtIndex(self: Model, index: int) = let ind = self.createIndex(index, 0, nil) @@ -369,8 +373,12 @@ QtObject: var roles: seq[int] if(self.items[i].senderId == contactId): - roles = @[ModelRole.SenderDisplayName.int, ModelRole.SenderLocalName.int, - ModelRole.SenderIcon.int, ModelRole.SenderIsAdded.int, ModelRole.SenderTrustStatus.int] + roles = @[ModelRole.SenderDisplayName.int, + ModelRole.SenderLocalName.int, + ModelRole.SenderIcon.int, + ModelRole.SenderIsAdded.int, + ModelRole.SenderTrustStatus.int, + ModelRole.SenderEnsVerified.int] if(self.items[i].pinnedBy == contactId): roles.add(ModelRole.PinnedBy.int) if(self.items[i].messageContainsMentions): diff --git a/ui/StatusQ b/ui/StatusQ index 08c53e2f526..ad2483ee241 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit 08c53e2f52648f16dfb0558e6f4cd8d2887ef154 +Subproject commit ad2483ee2415b54895fa9dd56ac0167a1b461176 diff --git a/ui/app/AppLayouts/Chat/controls/activityCenter/ChannelBadge.qml b/ui/app/AppLayouts/Chat/controls/activityCenter/ChannelBadge.qml index b9165065159..0dd6dbee032 100644 --- a/ui/app/AppLayouts/Chat/controls/activityCenter/ChannelBadge.qml +++ b/ui/app/AppLayouts/Chat/controls/activityCenter/ChannelBadge.qml @@ -50,8 +50,8 @@ Item { StyledText { id: contactInfo text: realChatType !== Constants.chatType.publicChat ? - StatusQUtils.Emoji.parse(Utils.removeStatusEns(Utils.filterXSS(name))) : - "#" + Utils.filterXSS(name) + StatusQUtils.Emoji.parse(Utils.removeStatusEns(StatusQUtils.Utils.filterXSS(name))) : + "#" + StatusQUtils.Utils.filterXSS(name) anchors.left: contactImage.right anchors.leftMargin: 4 color: textColor diff --git a/ui/app/AppLayouts/Chat/controls/activityCenter/ReplyComponent.qml b/ui/app/AppLayouts/Chat/controls/activityCenter/ReplyComponent.qml index 0ae8dce3ee2..2933c6df82f 100644 --- a/ui/app/AppLayouts/Chat/controls/activityCenter/ReplyComponent.qml +++ b/ui/app/AppLayouts/Chat/controls/activityCenter/ReplyComponent.qml @@ -24,7 +24,7 @@ Item { } StyledTextEdit { - text: Utils.getReplyMessageStyle(StatusQUtils.Emoji.parse(Utils.linkifyAndXSS(repliedMessageContent), StatusQUtils.Emoji.size.small), false) + text: Utils.getReplyMessageStyle(StatusQUtils.Emoji.parse(StatusQUtils.Utils.linkifyAndXSS(repliedMessageContent), StatusQUtils.Emoji.size.small), false) textFormat: Text.RichText height: 18 width: implicitWidth > 300 ? 300 : implicitWidth diff --git a/ui/app/AppLayouts/Chat/panels/UserListPanel.qml b/ui/app/AppLayouts/Chat/panels/UserListPanel.qml index ad590d0b9c6..50173c695a6 100644 --- a/ui/app/AppLayouts/Chat/panels/UserListPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/UserListPanel.qml @@ -96,14 +96,12 @@ Item { if (mouse.button === Qt.RightButton) { // Set parent, X & Y positions for the messageContextMenu messageContextMenu.parent = this - messageContextMenu.setXPosition = function() { return 0; } - messageContextMenu.setYPosition = function() { return mouse.y + (Style.current.halfPadding/2); } messageContextMenu.isProfile = true messageContextMenu.myPublicKey = userProfile.pubKey messageContextMenu.selectedUserPublicKey = model.pubKey messageContextMenu.selectedUserDisplayName = model.displayName messageContextMenu.selectedUserIcon = image.source - messageContextMenu.popup() + messageContextMenu.popup(4, 4) } else if (mouse.button === Qt.LeftButton && !!messageContextMenu) { Global.openProfilePopup(model.pubKey); } diff --git a/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml b/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml index d7da4e06828..c9fde003c57 100644 --- a/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml @@ -98,13 +98,16 @@ ModalPopup { delegate: Item { id: messageDelegate - property var listView: ListView.view - width: parent.width + + width: ListView.view.width height: messageItem.height MessageView { id: messageItem - store: popup.store + + width: parent.width + + rootStore: popup.store messageStore: popup.messageStore messageContextMenu: msgContextMenu @@ -113,9 +116,10 @@ ModalPopup { senderId: model.senderId senderDisplayName: model.senderDisplayName senderLocalName: model.senderLocalName + senderEnsName: model.senderEnsVerified ? model.senderDisplayName : "" senderIcon: model.senderIcon amISender: model.amISender - message: model.messageText + messageText: model.messageText messageImage: model.messageImage messageTimestamp: model.timestamp messageOutgoingStatus: model.outgoingStatus @@ -125,7 +129,6 @@ ModalPopup { reactionsModel: model.reactions senderTrustStatus: model.senderTrustStatus linkUrls: model.links - isInPinnedPopup: true transactionParams: model.transactionParameters // This is possible since we have all data loaded before we load qml. @@ -136,7 +139,8 @@ ModalPopup { nextMessageAsJsonObj: popup.messageStore? popup.messageStore.getMessageByIndexAsJson(index + 1) : {} // Additional params - forceHoverHandler: !popup.messageToPin + isInPinnedPopup: true + disableHover: !!popup.messageToPin } MouseArea { diff --git a/ui/app/AppLayouts/Chat/popups/community/CreateCategoryPopup.qml b/ui/app/AppLayouts/Chat/popups/community/CreateCategoryPopup.qml index d6fb2d521c7..03317f61361 100644 --- a/ui/app/AppLayouts/Chat/popups/community/CreateCategoryPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/community/CreateCategoryPopup.qml @@ -4,6 +4,7 @@ import QtQuick.Dialogs 1.3 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 @@ -216,9 +217,9 @@ StatusModal { let error = "" if (isEdit) { - error = root.store.editCommunityCategory(root.categoryId, Utils.filterXSS(root.contentItem.categoryName.input.text), JSON.stringify(channels)); + error = root.store.editCommunityCategory(root.categoryId, StatusQUtils.Utils.filterXSS(root.contentItem.categoryName.input.text), JSON.stringify(channels)); } else { - error = root.store.createCommunityCategory(Utils.filterXSS(root.contentItem.categoryName.input.text), JSON.stringify(channels)); + error = root.store.createCommunityCategory(StatusQUtils.Utils.filterXSS(root.contentItem.categoryName.input.text), JSON.stringify(channels)); } if (error) { diff --git a/ui/app/AppLayouts/Chat/popups/community/CreateChannelPopup.qml b/ui/app/AppLayouts/Chat/popups/community/CreateChannelPopup.qml index 74fc07fceb0..2049263bf11 100644 --- a/ui/app/AppLayouts/Chat/popups/community/CreateChannelPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/community/CreateChannelPopup.qml @@ -302,14 +302,14 @@ StatusModal { if (!isEdit) { //popup.contentItem.communityColor.color.toString().toUpperCase() - popup.createCommunityChannel(Utils.filterXSS(popup.contentItem.channelName.input.text), - Utils.filterXSS(popup.contentItem.channelDescription.input.text), + popup.createCommunityChannel(StatusQUtils.Utils.filterXSS(popup.contentItem.channelName.input.text), + StatusQUtils.Utils.filterXSS(popup.contentItem.channelDescription.input.text), emoji, popup.contentItem.channelColorDialog.color.toString().toUpperCase(), popup.categoryId) } else { - popup.editCommunityChannel(Utils.filterXSS(popup.contentItem.channelName.input.text), - Utils.filterXSS(popup.contentItem.channelDescription.input.text), + popup.editCommunityChannel(StatusQUtils.Utils.filterXSS(popup.contentItem.channelName.input.text), + StatusQUtils.Utils.filterXSS(popup.contentItem.channelDescription.input.text), emoji, popup.contentItem.channelColorDialog.color.toString().toUpperCase(), popup.categoryId) diff --git a/ui/app/AppLayouts/Chat/stores/MessageStore.qml b/ui/app/AppLayouts/Chat/stores/MessageStore.qml index ca298a98254..ac4d3f404f9 100644 --- a/ui/app/AppLayouts/Chat/stores/MessageStore.qml +++ b/ui/app/AppLayouts/Chat/stores/MessageStore.qml @@ -135,49 +135,6 @@ QtObject { return messageModule.toggleReaction(messageId, emojiId) } - function lastTwoItems(nodes) { - return nodes.join(qsTr(" and ")); - } - - function showReactionAuthors(jsonArrayOfUsersReactedWithThisEmoji, emojiId) { - let listOfUsers = JSON.parse(jsonArrayOfUsersReactedWithThisEmoji) - if (listOfUsers.error) { - console.error("error parsing users who reacted to a message, error: ", obj.error) - return - } - - let tooltip - if (listOfUsers.length === 1) { - tooltip = listOfUsers[0] - } else if (listOfUsers.length === 2) { - tooltip = lastTwoItems(listOfUsers); - } else { - var leftNode = []; - var rightNode = []; - const maxReactions = 12 - let maximum = Math.min(maxReactions, listOfUsers.length) - - if (listOfUsers.length > maxReactions) { - leftNode = listOfUsers.slice(0, maxReactions); - rightNode = listOfUsers.slice(maxReactions, listOfUsers.length); - return (rightNode.length === 1) ? - lastTwoItems([leftNode.join(", "), rightNode[0]]) : - lastTwoItems([leftNode.join(", "), qsTr("%1 more").arg(rightNode.length)]); - } - - leftNode = listOfUsers.slice(0, maximum - 1); - rightNode = listOfUsers.slice(maximum - 1, listOfUsers.length); - tooltip = lastTwoItems([leftNode.join(", "), rightNode[0]]) - } - - tooltip += qsTr(" reacted with "); - let emojiHtml = StatusQUtils.Emoji.getEmojiFromId(emojiId); - if (emojiHtml) { - tooltip += emojiHtml; - } - return tooltip - } - function deleteMessage(messageId) { if(!messageModule) return diff --git a/ui/app/AppLayouts/Chat/views/ActivityCenterGroupRequest.qml b/ui/app/AppLayouts/Chat/views/ActivityCenterGroupRequest.qml index 41673f86525..c2515fb99af 100644 --- a/ui/app/AppLayouts/Chat/views/ActivityCenterGroupRequest.qml +++ b/ui/app/AppLayouts/Chat/views/ActivityCenterGroupRequest.qml @@ -12,6 +12,7 @@ import "../controls" import "../panels" import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 import StatusQ.Core.Theme 0.1 Item { @@ -24,13 +25,17 @@ Item { property bool hideReadNotifications: false property bool acCurrentFilterAll: false - DateGroup { + StatusDateGroupLabel { id: dateGroupLbl + anchors.top: parent.top + anchors.topMargin: Style.current.halfPadding + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + height: visible ? implicitHeight : 0 + visible: text !== "" previousMessageIndex: root.previousNotificationIndex previousMessageTimestamp: root.previousNotificationTimestamp messageTimestamp: model.timestamp - isActivityCenterMessage: true - height: visible ? implicitHeight : 0 } Rectangle { diff --git a/ui/app/AppLayouts/Chat/views/ActivityCenterMessageComponentView.qml b/ui/app/AppLayouts/Chat/views/ActivityCenterMessageComponentView.qml index da99b2045eb..3b21f190cd3 100644 --- a/ui/app/AppLayouts/Chat/views/ActivityCenterMessageComponentView.qml +++ b/ui/app/AppLayouts/Chat/views/ActivityCenterMessageComponentView.qml @@ -124,11 +124,11 @@ Item { MessageView { id: notificationMessage anchors.right: undefined - store: root.store + rootStore: root.store messageStore: root.store.messageStore messageId: model.id senderDisplayName: model.message.senderDisplayName - message: model.message.messageText + messageText: model.message.messageText responseToMessageWithId: model.message.responseToMessageWithId senderId: model.message.senderId senderLocalName: model.message.senderLocalName @@ -140,10 +140,10 @@ Item { messageContentType: model.message.contentType senderTrustStatus: model.message.senderTrustStatus activityCenterMessage: true - read: model.read + activityCenterMessageRead: model.read onImageClicked: Global.openImagePopup(image, root.messageContextMenu) scrollToBottom: null - clickMessage: function (isProfileClick) { + messageClickHandler: { if (isProfileClick) { return Global.openProfilePopup(model.message.senderId); } diff --git a/ui/app/AppLayouts/Chat/views/ChatContentView.qml b/ui/app/AppLayouts/Chat/views/ChatContentView.qml index 83af946189e..de644f56063 100644 --- a/ui/app/AppLayouts/Chat/views/ChatContentView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatContentView.qml @@ -91,11 +91,10 @@ ColumnLayout { case Constants.chatType.publicChat: return qsTr("Public chat") case Constants.chatType.privateGroupChat: - let cnt = root.usersStore.usersModule.model.count - if(cnt > 1) return qsTr("%1 members").arg(cnt); - return qsTr("1 member"); + const cnt = root.usersStore.usersModule.model.count + return qsTr("%n members(s)", "", cnt) case Constants.chatType.communityChat: - return Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim() + return StatusQUtils.Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim() default: return "" } @@ -161,10 +160,11 @@ ColumnLayout { Component { id: contactsSelector GroupChatPanel { - sectionModule: root.chatSectionModule + sectionModule: chatSectionModule chatContentModule: root.chatContentModule rootStore: root.rootStore maxHeight: root.height + onPanelClosed: topBar.toolbarComponent = statusChatInfoButton } } @@ -427,15 +427,16 @@ ColumnLayout { id: chatMessages Layout.fillWidth: true Layout.fillHeight: true - store: root.rootStore + chatContentModule: root.chatContentModule + rootStore: root.rootStore contactsStore: root.contactsStore - messageContextMenuInst: contextmenu + messageContextMenu: contextmenu messageStore: messageStore emojiPopup: root.emojiPopup usersStore: root.usersStore stickersLoaded: root.stickersLoaded isChatBlocked: root.isBlocked - channelEmoji: chatContentModule.chatDetails.emoji || "" + channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "") isActiveChannel: root.isActiveChannel onShowReplyArea: { let obj = messageStore.getMessageByIdAsJson(messageId) @@ -454,8 +455,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom Layout.fillWidth: true Layout.preferredWidth: parent.width - height: chatInput.height - Layout.preferredHeight: height + Layout.preferredHeight: chatInput.implicitHeight + chatInput.anchors.leftMargin + chatInput.anchors.rightMargin Loader { id: loadingMessagesIndicator @@ -472,6 +472,10 @@ ColumnLayout { StatusChatInput { id: chatInput + + anchors.fill: parent + anchors.margins: Style.current.smallPadding + store: root.rootStore usersStore: root.usersStore diff --git a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml index f52b4f916cd..f80083bd71f 100644 --- a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml @@ -19,12 +19,18 @@ import shared.status 1.0 import shared.controls 1.0 import shared.views.chat 1.0 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + import "../controls" Item { id: root - property var store + property var chatContentModule + property var rootStore property var messageStore property var usersStore property var contactsStore @@ -37,7 +43,7 @@ Item { property bool isChatBlocked: false property bool isActiveChannel: false - property var messageContextMenuInst + property var messageContextMenu property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight property int newMessages: 0 @@ -67,23 +73,23 @@ Item { } // Not Refactored Yet -// onNewMessagePushed: { -// if (!chatLogView.scrollToBottom()) { -// newMessages++ -// } -// } + // onNewMessagePushed: { + // if (!chatLogView.scrollToBottom()) { + // newMessages++ + // } + // } } Item { id: loadingMessagesIndicator - visible: root.store.loadingHistoryMessagesInProgress + visible: root.rootStore.loadingHistoryMessagesInProgress anchors.top: parent.top anchors.left: parent.left height: visible? 20 : 0 width: parent.width Loader { - active: root.store.loadingHistoryMessagesInProgress + active: root.rootStore.loadingHistoryMessagesInProgress anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter sourceComponent: Component { @@ -105,14 +111,6 @@ Item { spacing: 0 verticalLayoutDirection: ListView.BottomToTop - // This header and Connections is to create an invisible padding so that the chat identifier is at the top - // The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height - // If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections - header: Item { - height: 0 - width: chatLogView.width - } - function checkHeaderHeight() { if (!chatLogView.headerItem) { return @@ -125,30 +123,70 @@ Item { } } + function scrollToBottom(force, caller) { + if (!force && !chatLogView.atYEnd) { + // User has scrolled up, we don't want to scroll back + return false + } + if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) { + // If we have a caller, only accept its request if it's the last message + return false + } + // Call this twice and with a timer since the first scroll to bottom might have happened before some stuff loads + // meaning that the scroll will not actually be at the bottom on switch + // Add a small delay because images, even though they say they say they are loaed, they aren't shown yet + Qt.callLater(chatLogView.positionViewAtBeginning) + timer.setTimeout(function() { + Qt.callLater(chatLogView.positionViewAtBeginning) + }, 100); + return true + } + + model: messageStore.messagesModel + + Component.onCompleted: chatLogView.scrollToBottom(true) + + onContentYChanged: { + scrollDownButton.visible = contentHeight - (scrollY + height) > 400 + let loadMore = scrollDownButton.visible && scrollY < 500 + if(loadMore){ + messageStore.loadMoreMessages() + } + } + ScrollBar.vertical: StatusScrollBar { visible: chatLogView.visibleArea.heightRatio < 1 } -// Connections { -// id: contentHeightConnection -// enabled: true -// target: chatLogView -// onContentHeightChanged: { -// chatLogView.checkHeaderHeight() -// } -// onHeightChanged: { -// chatLogView.checkHeaderHeight() -// } -// } + // This header and Connections is to create an invisible padding so that the chat identifier is at the top + // The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height + // If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections + header: Item { + height: 0 + width: chatLogView.width + } + + // Connections { + // id: contentHeightConnection + // enabled: true + // target: chatLogView + // onContentHeightChanged: { + // chatLogView.checkHeaderHeight() + // } + // onHeightChanged: { + // chatLogView.checkHeaderHeight() + // } + // } Timer { id: timer } Button { + id: scrollDownButton + readonly property int buttonPadding: 5 - id: scrollDownButton visible: false height: 32 width: nbMessages.width + arrowImage.width + 2 * Style.current.halfPadding + (nbMessages.visible ? scrollDownButton.buttonPadding : 0) @@ -160,6 +198,7 @@ Item { border.width: 0 radius: 16 } + onClicked: { newMessages = 0 scrollDownButton.visible = false @@ -202,51 +241,22 @@ Item { } } - function scrollToBottom(force, caller) { - if (!force && !chatLogView.atYEnd) { - // User has scrolled up, we don't want to scroll back - return false - } - if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) { - // If we have a caller, only accept its request if it's the last message - return false - } - // Call this twice and with a timer since the first scroll to bottom might have happened before some stuff loads - // meaning that the scroll will not actually be at the bottom on switch - // Add a small delay because images, even though they say they say they are loaed, they aren't shown yet - Qt.callLater(chatLogView.positionViewAtBeginning) - timer.setTimeout(function() { - Qt.callLater(chatLogView.positionViewAtBeginning) - }, 100); - return true - } - -// Connections { + // Connections { // Not Refactored Yet -// target: root.store.chatsModelInst - -// onAppReady: { -// chatLogView.scrollToBottom(true) -// } -// } - - onContentYChanged: { - scrollDownButton.visible = contentHeight - (scrollY + height) > 400 - let loadMore = scrollDownButton.visible && scrollY < 500 - if(loadMore){ - messageStore.loadMoreMessages() - } - } + // target: root.rootStore.chatsModelInst - model: messageStore.messagesModel - - Component.onCompleted: chatLogView.scrollToBottom(true) + // onAppReady: { + // chatLogView.scrollToBottom(true) + // } + // } delegate: MessageView { id: msgDelegate - objectName: "chatMessageViewDelegate" - store: root.store + width: ListView.view.width + + objectName: "chatMessageViewDelegate" + rootStore: root.rootStore messageStore: root.messageStore usersStore: root.usersStore contactsStore: root.contactsStore @@ -256,7 +266,7 @@ Item { isActiveChannel: root.isActiveChannel isChatBlocked: root.isChatBlocked - messageContextMenu: messageContextMenuInst + messageContextMenu: root.messageContextMenu itemIndex: index messageId: model.id @@ -265,10 +275,11 @@ Item { senderId: model.senderId senderDisplayName: model.senderDisplayName senderLocalName: model.senderLocalName + senderEnsName: model.senderEnsVerified ? model.senderDisplayName : "" senderIcon: model.senderIcon senderIsAdded: model.senderIsAdded amISender: model.amISender - message: model.messageText + messageText: model.messageText messageImage: model.messageImage messageTimestamp: model.timestamp messageOutgoingStatus: model.outgoingStatus @@ -283,6 +294,7 @@ Item { isEdited: model.isEdited linkUrls: model.links transactionParams: model.transactionParameters + hasMention: model.mentionedUsersPks.split(" ").includes(root.rootStore.userProfileInst.pubKey) gapFrom: model.gapFrom gapTo: model.gapTo @@ -304,7 +316,7 @@ Item { root.showReplyArea(messageId, author) } - onImageClicked: Global.openImagePopup(image, messageContextMenuInst) + onImageClicked: Global.openImagePopup(image, messageContextMenu) stickersLoaded: root.stickersLoaded diff --git a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml index 7b07426662e..0a5fc95a533 100644 --- a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml @@ -10,6 +10,7 @@ import shared.popups 1.0 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils import StatusQ.Layout 0.1 import StatusQ.Components 0.1 import StatusQ.Controls 0.1 @@ -148,10 +149,10 @@ StatusAppTwoPanelLayout { onEdited: { const error = root.chatCommunitySectionModule.editCommunity( - Utils.filterXSS(item.name), - Utils.filterXSS(item.description), - Utils.filterXSS(item.introMessage), - Utils.filterXSS(item.outroMessage), + StatusQUtils.Utils.filterXSS(item.name), + StatusQUtils.Utils.filterXSS(item.description), + StatusQUtils.Utils.filterXSS(item.introMessage), + StatusQUtils.Utils.filterXSS(item.outroMessage), item.options.requestToJoinEnabled ? Constants.communityChatOnRequestAccess : Constants.communityChatPublicAccess, item.color.toString().toUpperCase(), item.selectedTags, diff --git a/ui/app/AppLayouts/CommunitiesPortal/popups/CreateCommunityPopup.qml b/ui/app/AppLayouts/CommunitiesPortal/popups/CreateCommunityPopup.qml index 6718e3221b1..93a27a262b8 100644 --- a/ui/app/AppLayouts/CommunitiesPortal/popups/CreateCommunityPopup.qml +++ b/ui/app/AppLayouts/CommunitiesPortal/popups/CreateCommunityPopup.qml @@ -10,6 +10,7 @@ import shared.popups 1.0 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 @@ -154,10 +155,10 @@ StatusStackModal { function createCommunity() { const error = store.createCommunity({ - name: Utils.filterXSS(nameInput.input.text), - description: Utils.filterXSS(descriptionTextInput.input.text), - introMessage: Utils.filterXSS(introMessageInput.input.text), - outroMessage: Utils.filterXSS(outroMessageInput.input.text), + name: StatusQUtils.Utils.filterXSS(nameInput.input.text), + description: StatusQUtils.Utils.filterXSS(descriptionTextInput.input.text), + introMessage: StatusQUtils.Utils.filterXSS(introMessageInput.input.text), + outroMessage: StatusQUtils.Utils.filterXSS(outroMessageInput.input.text), color: colorPicker.color.toString().toUpperCase(), tags: communityTagsPicker.selectedTags, image: { diff --git a/ui/app/AppLayouts/Profile/views/AppearanceView.qml b/ui/app/AppLayouts/Profile/views/AppearanceView.qml index 200998a1683..89ed5c11902 100644 --- a/ui/app/AppLayouts/Profile/views/AppearanceView.qml +++ b/ui/app/AppLayouts/Profile/views/AppearanceView.qml @@ -56,23 +56,25 @@ SettingsContentBase { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - height: paceholderMessage.height + paceholderMessage.anchors.margins*2 + height: placeholderMessage.implicitHeight + + placeholderMessage.anchors.leftMargin + + placeholderMessage.anchors.rightMargin radius: Style.current.radius border.color: Style.current.border color: Style.current.transparent MessageView { - id: paceholderMessage + id: placeholderMessage anchors.top: parent.top anchors.left: parent.left - anchors.right: parent.left + anchors.right: parent.right anchors.margins: Style.current.smallPadding isMessage: true shouldRepeatHeader: true - messageTimestamp:Date.now() + messageTimestamp: Date.now() senderDisplayName: "@vitalik" senderIcon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAb0lEQVR4Ae3UQQqAIBRF0Wj9ba9Bq6l5JBQqfn/ngDMH3YS3AAB/tO3H+XRG3b9bR/+gVoREI2RapVXpfd5+X5oXERKNkHS+rk3tOpWkeREh0QiZVu91ql2zNC8iJBoh0yqtSqt1slpCghICANDPBc0ESPh0bHkHAAAAAElFTkSuQmCC" - message: qsTr("Blockchains will drop search costs, causing a kind of decomposition that allows you to have markets of entities that are horizontally segregated and vertically segregated.") + messageText: qsTr("Blockchains will drop search costs, causing a kind of decomposition that allows you to have markets of entities that are horizontally segregated and vertically segregated.") messageContentType: Constants.messageContentType.messageType placeholderMessage: true } diff --git a/ui/app/AppLayouts/stores/MessageStore.qml b/ui/app/AppLayouts/stores/MessageStore.qml index fc5cac6825f..d963b999aec 100644 --- a/ui/app/AppLayouts/stores/MessageStore.qml +++ b/ui/app/AppLayouts/stores/MessageStore.qml @@ -130,56 +130,4 @@ QtObject { return [] } } - property var clickMessage: function(isProfileClick, isSticker = false, isImage = false, image = null, isEmoji = false, hideEmojiPicker = false, isReply = false, isRightClickOnImage = false, imageSource = "") { - if (placeholderMessage || activityCenterMessage) { - return - } - - if (!isProfileClick) { - SelectedMessage.set(messageId, fromAuthor); - } - - messageContextMenu.messageId = messageId - messageContextMenu.contentType = contentType - messageContextMenu.linkUrls = linkUrls; - messageContextMenu.isProfile = !!isProfileClick; - messageContextMenu.isCurrentUser = isCurrentUser - messageContextMenu.isText = isText - messageContextMenu.isSticker = isSticker; - messageContextMenu.isEmoji = isEmoji; - messageContextMenu.hideEmojiPicker = hideEmojiPicker; - messageContextMenu.pinnedMessage = pinnedMessage; - messageContextMenu.isCurrentUser = isCurrentUser; - messageContextMenu.isRightClickOnImage = isRightClickOnImage - messageContextMenu.imageSource = imageSource - messageContextMenu.onClickEdit = function() {isEdit = true} - - //TODO remove dynamic scoping - if (isReply) { - let nickname = appMain.getUserNickname(repliedMessageAuthor) - messageContextMenu.show(repliedMessageAuthor, repliedMessageAuthorPubkey, repliedMessageUserImage, plainText, nickname, emojiReactionsModel); - } else { - let nickname = appMain.getUserNickname(fromAuthor) - messageContextMenu.show(userName, fromAuthor, profileImageSource, plainText, nickname, emojiReactionsModel); - } - - messageContextMenu.x = messageContextMenu.setXPosition() - messageContextMenu.y = messageContextMenu.setYPosition() - } - - function setHovered(messageId, hovered) { - if (hovered) { - hoveredMessage = messageId; - } else if (hoveredMessage === messageId) { - hoveredMessage = ""; - } - } - - function setMessageActive(messageId, active) { - if (active) { - activeMessage = messageId; - } else if (activeMessage === messageId) { - activeMessage = ""; - } - } } diff --git a/ui/imports/shared/controls/chat/DateGroup.qml b/ui/imports/shared/controls/chat/DateGroup.qml deleted file mode 100644 index 6efdb767894..00000000000 --- a/ui/imports/shared/controls/chat/DateGroup.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick 2.3 - -import shared 1.0 -import shared.panels 1.0 -import utils 1.0 - -StyledText { - property bool isActivityCenterMessage: false - property int previousMessageIndex: -1 - property string previousMessageTimestamp - property string messageTimestamp - - id: dateGroupLbl - font.pixelSize: 13 - color: Style.current.secondaryText - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: isActivityCenterMessage ? undefined : parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: visible ? (isActivityCenterMessage ? Style.current.halfPadding : 20) : 0 - anchors.left: parent.left - anchors.leftMargin: isActivityCenterMessage ? Style.current.padding : 0 - - text: { - if (previousMessageIndex === -1) return ""; // identifier - - let now = new Date() - let yesterday = new Date() - yesterday.setDate(now.getDate()-1) - - let currentMsgDate = new Date(parseInt(messageTimestamp, 10)); - let prevMsgDate = previousMessageTimestamp === "" ? undefined : new Date(parseInt(previousMessageTimestamp, 10)); - - if (!!prevMsgDate && currentMsgDate.getDay() === prevMsgDate.getDay()) { - return "" - } - - if (now.toDateString() === currentMsgDate.toDateString()) { - return qsTr("Today") - } else if (yesterday.toDateString() === currentMsgDate.toDateString()) { - return qsTr("Yesterday") - } else { - const monthNames = [ - qsTr("January"), - qsTr("February"), - qsTr("March"), - qsTr("April"), - qsTr("May"), - qsTr("June"), - qsTr("July"), - qsTr("August"), - qsTr("September"), - qsTr("October"), - qsTr("November"), - qsTr("December") - ]; - return monthNames[currentMsgDate.getMonth()] + ", " + currentMsgDate.getDate() - } - } - visible: text !== "" -} diff --git a/ui/imports/shared/controls/chat/MessageMouseArea.qml b/ui/imports/shared/controls/chat/MessageMouseArea.qml deleted file mode 100644 index 0c5e51d90fb..00000000000 --- a/ui/imports/shared/controls/chat/MessageMouseArea.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick 2.13 - -import utils 1.0 -import shared 1.0 - -MouseArea { - id: mouseArea - z: 50 - enabled: !placeholderMessage -//TODO remove dynamic scoping -// property bool isSticker: false -// property bool placeholderMessage: false - property bool isHovered: false - property bool stickersLoaded: false - property bool isMessageActive - property bool isActivityCenterMessage: false - property var messageContextMenu - property var messageContextMenuParent - signal openStickerPackPopup() - signal setMessageActive(string messageId, bool active) - signal clickMessage(bool isProfileClick, bool isSticker, bool isImage) - - cursorShape: !enabled ? Qt.PointingHandCursor : undefined - - acceptedButtons: Qt.LeftButton | Qt.RightButton - - onClicked: { - if (isActivityCenterMessage) { - mouseArea.clickMessage(false, isSticker, false) - return - } - if (mouse.button === Qt.RightButton) { - if (!!mouseArea.messageContextMenu) { - // Set parent, X & Y positions for the messageContextMenu - messageContextMenu.parent = messageContextMenuParent; - messageContextMenu.setXPosition = function() { return (mouse.x)}; - messageContextMenu.setYPosition = function() { return (mouse.y)}; - } - mouseArea.clickMessage(false, isSticker, false) - if (typeof isMessageActive !== "undefined") { - setMessageActive(messageId, true) - } - return; - } - if (mouse.button === Qt.LeftButton && isSticker && stickersLoaded) { - if (isHovered) { - isHovered = false; - } - openStickerPackPopup(); - return; - } - } -} diff --git a/ui/imports/shared/controls/chat/Retry.qml b/ui/imports/shared/controls/chat/Retry.qml deleted file mode 100644 index d263a492dbd..00000000000 --- a/ui/imports/shared/controls/chat/Retry.qml +++ /dev/null @@ -1,24 +0,0 @@ -import QtQuick 2.3 - -import shared 1.0 -import shared.panels 1.0 -import utils 1.0 - -StyledText { - id: retryLbl - color: Style.current.red - text: qsTr("Resend") - font.pixelSize: Style.current.tertiaryTextFontSize - visible: isCurrentUser && (timeout || isExpired) - property bool isCurrentUser: false - property bool isExpired: false - property bool timeout: false - signal clicked() - MouseArea { - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - onClicked: { - retryLbl.clicked(); - } - } -} diff --git a/ui/imports/shared/controls/chat/UserImage.qml b/ui/imports/shared/controls/chat/UserImage.qml index fc8ddd768db..17b512fabdc 100644 --- a/ui/imports/shared/controls/chat/UserImage.qml +++ b/ui/imports/shared/controls/chat/UserImage.qml @@ -18,7 +18,6 @@ Loader { property string image property bool showRing: true property bool interactive: true - property var messageContextMenu property int colorId: Utils.colorIdForPubkey(pubkey) property var colorHash: Utils.getColorHashAsJson(pubkey) @@ -51,12 +50,6 @@ Loader { cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { - if (!!root.messageContextMenu) { - // Set parent, X & Y positions for the messageContextMenu - root.messageContextMenu.parent = root - root.messageContextMenu.setXPosition = function() { return root.width + 4 } - root.messageContextMenu.setYPosition = function() { return 0 } - } root.clicked() } } diff --git a/ui/imports/shared/controls/chat/UsernameLabel.qml b/ui/imports/shared/controls/chat/UsernameLabel.qml index 1af72d7f2dd..c68ec877239 100644 --- a/ui/imports/shared/controls/chat/UsernameLabel.qml +++ b/ui/imports/shared/controls/chat/UsernameLabel.qml @@ -12,7 +12,6 @@ Item { width: chatName.width + (ensOrAlias.visible ? ensOrAlias.width + ensOrAlias.anchors.leftMargin : 0) property alias label: chatName - property var messageContextMenu property string displayName property string localName property bool amISender @@ -41,12 +40,6 @@ Item { root.isHovered = false } onClicked: { - if (!!root.messageContextMenu) { - // Set parent, X & Y positions for the messageContextMenu - root.messageContextMenu.parent = root - root.messageContextMenu.setXPosition = function() { return 0} - root.messageContextMenu.setYPosition = function() { return root.height + 4} - } root.clickMessage(true); } } diff --git a/ui/imports/shared/panels/chat/AudioPlayerPanel.qml b/ui/imports/shared/panels/chat/AudioPlayerPanel.qml deleted file mode 100644 index a73b34902fe..00000000000 --- a/ui/imports/shared/panels/chat/AudioPlayerPanel.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick 2.3 -import QtMultimedia 5.14 -import shared 1.0 -import shared.panels 1.0 -import shared.stores 1.0 - -import utils 1.0 - -Item { - property string audioSource: "" - - height: 20 - width: 350 - - Audio { - id: audioMessage - source: audioSource - store: RootStore - notifyInterval: 150 - } - - SVGImage { - id: playButton - source: audioMessage.playbackState == Audio.PlayingState ? Style.svg("icon-pause") : Style.svg("icon-play") - width: 15 - height: 15 - anchors.left: parent.left - anchors.leftMargin: Style.current.padding - anchors.verticalCenter: parent.verticalCenter - MouseArea { - id: playArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onPressed: { - if(audioMessage.playbackState === Audio.PlayingState){ - audioMessage.pause(); - } else { - audioMessage.play(); - } - - } - } - } - - Rectangle { - height: 2 - width: 300 - color: Style.current.grey - anchors.verticalCenter: parent.verticalCenter - anchors.left: playButton.right - anchors.leftMargin: 20 - Rectangle { - id: progress - height: 2 - width: { - if(audioMessage.duration === 0) return 0; - if(audioMessage.playbackState === Audio.StoppedState) return 0; - return parent.width * audioMessage.position / audioMessage.duration; - } - color: Style.current.black - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - id: handle - width: 10 - height: 10 - color: Style.current.black - radius: 10 - anchors.verticalCenter: parent.verticalCenter - x: progress.width - state: "default" - - states: State { - name: "pressed" - when: handleMouseArea.pressed - PropertyChanges { - target: handle; - scale: 1.2 - } - } - transitions: Transition { - NumberAnimation { - properties: "scale"; - duration: 100; - easing.type: Easing.InOutQuad - } - } - - MouseArea { - id: handleMouseArea - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - drag.target: parent - drag.axis: Drag.XAxis - drag.minimumX: 0 - drag.maximumX: parent.parent.width - onPressed: { - handle.state = "pressed" - if(audioMessage.playbackState === Audio.PlayingState){ - audioMessage.pause(); - } - } - onReleased: { - handle.state = "default" - audioMessage.seek(audioMessage.duration * handle.x / parent.parent.width); - } - } - } - } -} diff --git a/ui/imports/shared/panels/chat/ChatButtonsPanel.qml b/ui/imports/shared/panels/chat/ChatButtonsPanel.qml deleted file mode 100644 index c96c300c770..00000000000 --- a/ui/imports/shared/panels/chat/ChatButtonsPanel.qml +++ /dev/null @@ -1,210 +0,0 @@ -import QtQuick 2.13 -import QtGraphicalEffects 1.13 - -import StatusQ.Controls 0.1 - -import utils 1.0 -import shared.popups 1.0 - -Rectangle { - id: buttonsContainer - property bool parentIsHovered: false - property bool isChatBlocked: false - property int containerMargin: 2 - property int contentType: 2 - property bool isCurrentUser: false - property bool isMessageActive: false - property var messageContextMenu - property bool isInPinnedPopup: false - property bool activityCenterMsg - property bool placeholderMsg - property string fromAuthor - property bool editBtnActive: false - property bool pinButtonActive: false - property bool deleteButtonActive: false - property bool pinnedMessage: false - property bool canPin: false - signal replyClicked(string messageId, string author) - signal hoverChanged(bool hovered) - signal setMessageActive(string messageId, bool active) - signal clickMessage(bool isProfileClick, bool isSticker, bool isImage, var image, bool isEmoji, bool hideEmojiPicker) - - visible: !buttonsContainer.isChatBlocked && - !buttonsContainer.placeholderMsg && !buttonsContainer.activityCenterMsg && - (buttonsContainer.parentIsHovered || isMessageActive) - && contentType !== Constants.messageContentType.transactionType - width: buttonRow.width + buttonsContainer.containerMargin * 2 - height: 36 - radius: Style.current.radius - color: Style.current.modalBackground - z: 52 - - layer.enabled: true - layer.effect: DropShadow { - width: buttonsContainer.width - height: buttonsContainer.height - x: buttonsContainer.x - y: buttonsContainer.y + 10 - visible: buttonsContainer.visible - source: buttonsContainer - horizontalOffset: 0 - verticalOffset: 2 - radius: 10 - samples: 15 - color: "#22000000" - } - - MouseArea { - anchors.fill: buttonsContainer - acceptedButtons: Qt.NoButton - hoverEnabled: true - onEntered: { - buttonsContainer.hoverChanged(true) - } - onExited: { - buttonsContainer.hoverChanged(false) - } - } - - Row { - id: buttonRow - spacing: buttonsContainer.containerMargin - anchors.left: parent.left - anchors.leftMargin: buttonsContainer.containerMargin - anchors.verticalCenter: buttonsContainer.verticalCenter - height: parent.height - 2 * buttonsContainer.containerMargin - - Loader { - active: !buttonsContainer.isInPinnedPopup - sourceComponent: StatusFlatRoundButton { - id: emojiBtn - width: 32 - height: 32 - icon.name: "reaction-b" - type: StatusFlatRoundButton.Type.Tertiary - tooltip.text: qsTr("Add reaction") - onClicked: { - setMessageActive(messageId, true) - // Set parent, X & Y positions for the messageContextMenu - buttonsContainer.messageContextMenu.parent = buttonsContainer - buttonsContainer.messageContextMenu.setXPosition = function() { return (-Math.abs(buttonsContainer.width - buttonsContainer.messageContextMenu.emojiContainer.width))} - buttonsContainer.messageContextMenu.setYPosition = function() { return (-buttonsContainer.messageContextMenu.height - 4)} - buttonsContainer.clickMessage(false, false, false, null, true, false) - } - onHoveredChanged: buttonsContainer.hoverChanged(this.hovered) - } - } - - Loader { - active: !buttonsContainer.isInPinnedPopup - sourceComponent: StatusFlatRoundButton { - id: replyBtn - width: 32 - height: 32 - icon.name: "reply" - type: StatusFlatRoundButton.Type.Tertiary - tooltip.text: qsTr("Reply") - onClicked: { - buttonsContainer.replyClicked(messageId, fromAuthor); - if (messageContextMenu.closeParentPopup) { - messageContextMenu.closeParentPopup() - } - } - onHoveredChanged: buttonsContainer.hoverChanged(this.hovered) - } - } - - Loader { - active: buttonsContainer.editBtnActive && !buttonsContainer.isInPinnedPopup - sourceComponent: StatusFlatRoundButton { - id: editButton - width: 32 - height: 32 - icon.source: Style.svg("edit-message") - type: StatusFlatRoundButton.Type.Tertiary - tooltip.text: qsTr("Edit") - onClicked: messageStore.setEditModeOn(messageId) - onHoveredChanged: buttonsContainer.hoverChanged(editButton.hovered) - } - } - - Loader { - active: buttonsContainer.pinButtonActive - sourceComponent: StatusFlatRoundButton { - id: pinButton - width: 32 - height: 32 - icon.name: buttonsContainer.pinnedMessage ? "unpin" : "pin" - type: StatusFlatRoundButton.Type.Tertiary - tooltip.text: buttonsContainer.pinnedMessage ? qsTr("Unpin") : qsTr("Pin") - onHoveredChanged: buttonsContainer.hoverChanged(pinButton.hovered) - onClicked: { - if (buttonsContainer.pinnedMessage) { - messageStore.unpinMessage(messageId) - return; - } - - if (buttonsContainer.canPin) { - messageStore.pinMessage(messageId) - return; - } - - if (!chatContentModule) { - console.warn("error on open pinned messages limit reached from message context menu - chat content module is not set") - return; - } - - Global.openPopup(pinnedMessagesPopupComponent, { - store: rootStore, - messageStore: messageStore, - pinnedMessagesModel: chatContentModule.pinnedMessagesModel, - messageToPin: buttonsContainer.messageId - }); - } - } - } - - Loader { - active: buttonsContainer.deleteButtonActive && !buttonsContainer.isInPinnedPopup - sourceComponent: StatusFlatRoundButton { - id: deleteButton - width: 32 - height: 32 - type: StatusFlatRoundButton.Type.Tertiary - icon.name: "delete" - tooltip.text: qsTr("Delete") - onHoveredChanged: buttonsContainer.hoverChanged(deleteButton.hovered) - onClicked: { - if (!localAccountSensitiveSettings.showDeleteMessageWarning) { - messageStore.deleteMessage(messageId) - } - else { - Global.openPopup(deleteMessageConfirmationDialogComponent) - } - } - } - } - - Component { - id: deleteMessageConfirmationDialogComponent - - ConfirmationDialog { - header.title: qsTr("Confirm deleting this message") - confirmationText: qsTr("Are you sure you want to delete this message? Be aware that other clients are not guaranteed to delete the message as well.") - height: 260 - checkbox.visible: true - executeConfirm: function () { - if (checkbox.checked) { - localAccountSensitiveSettings.showDeleteMessageWarning = false - } - - close() - messageStore.deleteMessage(messageId) - } - onClosed: { - destroy() - } - } - } - } -} diff --git a/ui/imports/shared/panels/chat/ChatReplyPanel.qml b/ui/imports/shared/panels/chat/ChatReplyPanel.qml deleted file mode 100644 index d52096e7d3e..00000000000 --- a/ui/imports/shared/panels/chat/ChatReplyPanel.qml +++ /dev/null @@ -1,192 +0,0 @@ -import QtQuick 2.14 -import QtQuick.Shapes 1.13 -import QtGraphicalEffects 1.13 - -import utils 1.0 -import shared.controls 1.0 -import shared 1.0 -import shared.panels 1.0 -import shared.status 1.0 -import shared.controls.chat 1.0 - -import StatusQ.Core.Utils 0.1 as StatusQUtils - -Loader { - id: root - - property bool amISenderOfTheRepliedMessage - property int repliedMessageContentType - property string repliedMessageSenderIcon - property bool repliedMessageIsEdited - property string repliedMessageSender - property string repliedMessageSenderPubkey - property bool repliedMessageSenderIsAdded - property string repliedMessageContent - property string repliedMessageImage - property bool isCurrentUser: false - property int nameMargin: 6 - property int textFieldWidth: item ? item.textField.width : 0 - property int textFieldImplicitWidth: 0 - property int authorWidth: item ? item.authorMetrics.width : 0 - property bool longReply: false - property color elementsColor: amISenderOfTheRepliedMessage ? Style.current.chatReplyCurrentUser : Style.current.secondaryText - property var container - property int chatHorizontalPadding - property string stickerData - - signal clickMessage(bool isProfileClick, bool isSticker, bool isImage, var image, bool isEmoji, bool hideEmojiPicker, bool isReply) - signal scrollToBottom(bool isit, var container) - - sourceComponent: Component { - Item { - property alias textField: lblReplyMessage - property alias authorMetrics: txtAuthorMetrics - - id: chatReply - // childrenRect.height shows a binding loop for some reason, so we use heights instead - height: { - const h = userImage.height + 4 - if (repliedMessageContentType === Constants.messageContentType.imageType) { - return h + imgReplyImage.height - } - if (repliedMessageContentType === Constants.messageContentType.stickerType) { - return h + stickerLoader.height - } - return h + lblReplyMessage.height - } - width: parent.width - clip: true - - TextMetrics { - id: txtAuthorMetrics - font: lblReplyAuthor.font - text: lblReplyAuthor.text - } - - Shape { - id: replyCorner - anchors.left: parent.left - anchors.leftMargin: 20 - 1 - anchors.top: parent.top - anchors.topMargin: Style.current.smallPadding - width: 20 - height: parent.height - anchors.topMargin - asynchronous: true - antialiasing: true - - ShapePath { - id: capTest - - strokeColor: Utils.setColorAlpha(root.elementsColor, 0.4) - strokeWidth: 3 - fillColor: "transparent" - - capStyle: ShapePath.RoundCap - joinStyle: ShapePath.RoundJoin - - startX: 20 - startY: 0 - PathLine { x: 10; y: 0 } - PathArc { - x: 0; y: 10 - radiusX: 13 - radiusY: 13 - direction: PathArc.Counterclockwise - } - PathLine { x: 0; y: chatReply.height - replyCorner.anchors.topMargin } - } - } - - UserImage { - id: userImage - anchors.left: replyCorner.right - anchors.leftMargin: Style.current.halfPadding - - imageHeight: 20 - imageWidth: 20 - active: true - - name: repliedMessageSender - pubkey: repliedMessageSenderPubkey - image: repliedMessageSenderIcon - - onClicked: root.clickMessage(true, false, false, null, false, false, true) - } - - StyledTextEdit { - id: lblReplyAuthor - text: repliedMessageSender - color: root.elementsColor - readOnly: true - font.pixelSize: Style.current.secondaryTextFontSize - selectByMouse: true - font.weight: Font.Medium - anchors.verticalCenter: userImage.verticalCenter - anchors.left: userImage.right - anchors.leftMargin: 5 - } - - StatusChatImage { - id: imgReplyImage - visible: repliedMessageContentType === Constants.messageContentType.imageType - imageWidth: 50 - imageSource: repliedMessageImage - anchors.top: lblReplyAuthor.bottom - anchors.topMargin: nameMargin - anchors.left: userImage.left - chatHorizontalPadding: 0 - container: root.container - allCornersRounded: true - playing: false - } - - Loader { - id: stickerLoader - active: repliedMessageContentType === Constants.messageContentType.stickerType - anchors.top: lblReplyAuthor.bottom - anchors.topMargin: nameMargin - anchors.left: userImage.left - sourceComponent: Component { - StatusSticker { - id: stickerId - imageHeight: 56 - imageWidth: 56 - stickerData: root.stickerData - contentType: repliedMessageContentType - onLoaded: { - scrollToBottom(true, root.container) - } - } - } - } - - StyledTextEdit { - id: lblReplyMessage - visible: repliedMessageContentType !== Constants.messageContentType.imageType && repliedMessageContentType !== Constants.messageContentType.stickerType - Component.onCompleted: textFieldImplicitWidth = implicitWidth - anchors.top: lblReplyAuthor.bottom - anchors.topMargin: nameMargin - text: { - if (repliedMessageIsEdited){ - let index = repliedMessageContent.length - 4 - return Utils.getReplyMessageStyle(StatusQUtils.Emoji.parse(Utils.linkifyAndXSS(repliedMessageContent.slice(0, index) + Constants.editLabel + repliedMessageContent.slice(index)), StatusQUtils.Emoji.size.small), amISenderOfTheRepliedMessage) - } else { - return Utils.getReplyMessageStyle(StatusQUtils.Emoji.parse(Utils.linkifyAndXSS(repliedMessageContent), StatusQUtils.Emoji.size.small), amISenderOfTheRepliedMessage) - } - } - textFormat: Text.RichText - color: root.elementsColor - readOnly: true - selectByMouse: true - font.pixelSize: Style.current.additionalTextSize - font.weight: Font.Medium - anchors.left: userImage.left - width: root.longReply ? parent.width : implicitWidth - height: 20 - clip: true - z: 51 - } - } - } -} - diff --git a/ui/imports/shared/panels/chat/EmojiReactionsPanel.qml b/ui/imports/shared/panels/chat/EmojiReactionsPanel.qml deleted file mode 100644 index b6c6bd59559..00000000000 --- a/ui/imports/shared/panels/chat/EmojiReactionsPanel.qml +++ /dev/null @@ -1,192 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 2.13 -import QtGraphicalEffects 1.13 - -import shared 1.0 -import shared.panels 1.0 - -import StatusQ.Controls 0.1 as StatusQ -import utils 1.0 - -Item { - id: root - height: 20 - width: childrenRect.width - - property int imageMargin: 4 - signal addEmojiClicked() - signal hoverChanged(bool hovered) - signal toggleReaction(int emojiID) - signal setMessageActive(string messageId, bool active) - - property var store - property bool isCurrentUser - property var emojiReactionsModel - property bool isMessageActive - - Row { - spacing: root.imageMargin - - Repeater { - id: reactionRepeater - width: childrenRect.width - model: root.emojiReactionsModel - - Rectangle { - property bool isHovered: false - - id: emojiContainer - width: emojiImage.width + emojiCount.width + (root.imageMargin * 2) + + 8 - height: 20 - radius: 10 - color: model.didIReactWithThisEmoji ? - (isHovered ? Style.current.emojiReactionActiveBackgroundHovered : Style.current.secondaryBackground) : - (isHovered ? Style.current.emojiReactionBackgroundHovered : Style.current.emojiReactionBackground) - - StatusQ.StatusToolTip { - visible: mouseArea.containsMouse - maxWidth: 400 - text: root.store.showReactionAuthors(model.jsonArrayOfUsersReactedWithThisEmoji, model.emojiId) - } - - // Rounded corner to cover one corner - Rectangle { - color: parent.color - width: 10 - height: 10 - anchors.top: parent.top - anchors.left: !root.isCurrentUser? parent.left : undefined - anchors.leftMargin: 0 - anchors.right: !root.isCurrentUser? undefined : parent.right - anchors.rightMargin: 0 - radius: 2 - z: -1 - } - - // This is a workaround to get a "border" around the rectangle including the weird rectangle - Loader { - active: model.didIReactWithThisEmoji - anchors.top: parent.top - anchors.topMargin: -1 - anchors.left: parent.left - anchors.leftMargin: -1 - z: -2 - - sourceComponent: Component { - Rectangle { - width: emojiContainer.width + 2 - height: emojiContainer.height + 2 - radius: emojiContainer.radius - color: Style.current.primary - - Rectangle { - color: parent.color - width: 10 - height: 10 - anchors.top: parent.top - anchors.left: !root.isCurrentUser? parent.left : undefined - anchors.leftMargin: 0 - anchors.right: !root.isCurrentUser? undefined : parent.right - anchors.rightMargin: 0 - radius: 2 - z: -1 - } - } - } - } - - SVGImage { - id: emojiImage - width: 15 - height: 15 - fillMode: Image.PreserveAspectFit - source: { - switch (model.emojiId) { - case 1: return Style.svg("emojiReactions/heart") - case 2: return Style.svg("emojiReactions/thumbsUp") - case 3: return Style.svg("emojiReactions/thumbsDown") - case 4: return Style.svg("emojiReactions/laughing") - case 5: return Style.svg("emojiReactions/sad") - case 6: return Style.svg("emojiReactions/angry") - default: return "" - } - } - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: root.imageMargin - } - - StyledText { - id: emojiCount - text: model.numberOfReactions - anchors.verticalCenter: parent.verticalCenter - anchors.left: emojiImage.right - anchors.leftMargin: root.imageMargin - font.pixelSize: 12 - color: model.didIReactWithThisEmoji ? Style.current.textColorTertiary : Style.current.textColor - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: { - root.hoverChanged(true) - emojiContainer.isHovered = true - } - onExited: { - root.hoverChanged(false) - emojiContainer.isHovered = false - } - onClicked: { - toggleReaction(model.emojiId) - } - } - } - } - - Item { - width: addEmojiBtn.width + addEmojiBtn.anchors.leftMargin // there is more margin between the button and the emojis than between each emoji - height: addEmojiBtn.height - - SVGImage { - property bool isHovered: false - - id: addEmojiBtn - source: Style.svg("emoji") - width: 16.5 - height: 16.5 - anchors.left: parent.left - anchors.leftMargin: 2.5 - - } - - ColorOverlay { - anchors.fill: addEmojiBtn - antialiasing: true - source: addEmojiBtn - color: addEmojiBtn.isHovered ? Style.current.primary : Style.current.secondaryText - } - - MouseArea { - anchors.fill: addEmojiBtn - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: addEmojiBtn.isHovered = true - onExited: addEmojiBtn.isHovered = false - onClicked: { - if (typeof root.isMessageActive !== "undefined") { - setMessageActive(messageId, true); - } - root.addEmojiClicked(); - } - } - - StatusQ.StatusToolTip { - visible: addEmojiBtn.isHovered - text: qsTr("Add reaction") - } - } - } -} diff --git a/ui/imports/shared/popups/ContactVerificationRequestPopup.qml b/ui/imports/shared/popups/ContactVerificationRequestPopup.qml index 2367d1b2fd0..56d614e1857 100644 --- a/ui/imports/shared/popups/ContactVerificationRequestPopup.qml +++ b/ui/imports/shared/popups/ContactVerificationRequestPopup.qml @@ -63,12 +63,13 @@ StatusModal { id: verificationMessage anchors.top: description.bottom anchors.topMargin: Style.current.padding + width: parent.width isMessage: true shouldRepeatHeader: true messageTimestamp: root.messageTimestamp senderDisplayName: root.senderDisplayName senderIcon: root.senderIcon - message: root.challengeText + messageText: root.challengeText messageContentType: Constants.messageContentType.messageType placeholderMessage: true } @@ -93,12 +94,13 @@ StatusModal { id: responseMessage visible: !!root.responseText anchors.top: verificationMessage.bottom + width: parent.width isMessage: true shouldRepeatHeader: true messageTimestamp: root.responseTimestamp senderDisplayName: userProfile.name senderIcon: userProfile.icon - message: root.responseText + messageText: root.responseText messageContentType: Constants.messageContentType.messageType placeholderMessage: true } diff --git a/ui/imports/shared/qmldir b/ui/imports/shared/qmldir index 186cb844ced..085a8629e7d 100644 --- a/ui/imports/shared/qmldir +++ b/ui/imports/shared/qmldir @@ -4,4 +4,3 @@ DelegateModelGeneralized 1.0 DelegateModelGeneralized.qml LoadingAnimation 1.0 LoadingAnimation.qml MacTrafficLights 1.0 MacTrafficLights.qml NumberPolyFill 1.0 polyfill.number.toLocaleString.js -XSS 1.0 xss.js diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index 0f1367ec3f3..9ecafc244b5 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -24,6 +24,7 @@ import StatusQ.Controls 0.1 as StatusQ Rectangle { id: control + signal sendTransactionCommandButtonClicked() signal receiveTransactionCommandButtonClicked() signal stickerSelected(string hashId, string packId) @@ -53,19 +54,18 @@ Rectangle { property var stickerPackList property int extraHeightFactor: calculateExtraHeightFactor() - property int messageLimit: control.isStatusUpdateInput ? 300 : 2000 - property int messageLimitVisible: control.isStatusUpdateInput ? 50 : 200 + property int messageLimit: 2000 + property int messageLimitVisible: 200 property int chatType property string chatInputPlaceholder: qsTr("Message") property alias textInput: messageInputField - property bool isStatusUpdateInput: chatType === Constants.chatType.profile property var fileUrls: [] - property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top + property var imageErrorMessageLocation: StatusChatInput.ImageErrorMessageLocation.Top // TODO: Remove this proeprty? property var messageContextMenu @@ -76,20 +76,50 @@ Rectangle { Bottom } - height: { - if (extendedArea.visible) { - return messageInput.height + extendedArea.height + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding) - } - if (messageInput.height > messageInput.defaultInputFieldHeight) { - if (messageInput.height >= messageInput.maxInputFieldHeight) { - return messageInput.maxInputFieldHeight + (control.isStatusUpdateInput ? 0 : Style.current.bigPadding) + function parseMessage(message) { + let mentionsMap = new Map() + let index = 0 + while (true) { + index = message.indexOf("", index) + 4 + if (endIndex < 0) { + index += 8 // " ' + mentionsMap.set(mentionLink, mentionTag) + index += linkTag.length } - return control.isStatusUpdateInput ? 56 : 64 + + let text = message; + + for (let [key, value] of mentionsMap) + text = text.replace(new RegExp(key, 'g'), value) + + textInput.text = text + textInput.cursorPosition = textInput.length } - anchors.left: parent.left - anchors.right: parent.right + + implicitWidth: layout.implicitWidth + layout.anchors.leftMargin + layout.anchors.rightMargin + implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin color: Style.current.transparent @@ -184,10 +214,6 @@ Rectangle { event.accepted = true; return } - - if (control.isStatusUpdateInput) { - return // Status update require the send button to be clicked - } if (messageInputField.length < messageLimit) { control.sendMessage(event) control.hideExtendedArea(); @@ -270,7 +296,7 @@ Rectangle { let lastCursorPosition = suggestionsBox.suggestionFilter.cursorPosition; let lastAtPosition = suggestionsBox.suggestionFilter.lastAtPosition; if (aliasName.toLowerCase() === suggestionsBox.suggestionsModel.get(suggestionsBox.listView.currentIndex).name.toLowerCase() - && (event.key !== Qt.Key_Backspace) && (event.key !== Qt.Key_Delete)) { + && (event.key !== Qt.Key_Backspace) && (event.key !== Qt.Key_Delete)) { insertMention(aliasName, lastAtPosition, lastCursorPosition); } else if (event.key === Qt.Key_Space) { var plainTextToReplace = messageInputField.getText(lastAtPosition, lastCursorPosition); @@ -295,30 +321,30 @@ Rectangle { } function unwrapSelection(unwrapWith, selectedTextWithFormationChars) { - if (messageInputField.selectionStart - messageInputField.selectionEnd === 0) return + if (messageInputField.selectionStart - messageInputField.selectionEnd === 0) return - // calulate the new selection start and end positions - var newSelectionStart = messageInputField.selectionStart - unwrapWith.length - var newSelectionEnd = messageInputField.selectionEnd-messageInputField.selectionStart + newSelectionStart + // calulate the new selection start and end positions + var newSelectionStart = messageInputField.selectionStart - unwrapWith.length + var newSelectionEnd = messageInputField.selectionEnd-messageInputField.selectionStart + newSelectionStart - selectedTextWithFormationChars = selectedTextWithFormationChars.trim() - // Check if the selectedTextWithFormationChars has formation chars and if so, calculate how many so we can adapt the start and end pos - const selectTextDiff = (selectedTextWithFormationChars.length - messageInputField.selectedText.length) / 2 + selectedTextWithFormationChars = selectedTextWithFormationChars.trim() + // Check if the selectedTextWithFormationChars has formation chars and if so, calculate how many so we can adapt the start and end pos + const selectTextDiff = (selectedTextWithFormationChars.length - messageInputField.selectedText.length) / 2 - // Remove the deselected option from the before and after the selected text - const prefixChars = messageInputField.getText((messageInputField.selectionStart - selectTextDiff), messageInputField.selectionStart) - const updatedPrefixChars = prefixChars.replace(unwrapWith, '') - const postfixChars = messageInputField.getText(messageInputField.selectionEnd, (messageInputField.selectionEnd + selectTextDiff)) - const updatedPostfixChars = postfixChars.replace(unwrapWith, '') + // Remove the deselected option from the before and after the selected text + const prefixChars = messageInputField.getText((messageInputField.selectionStart - selectTextDiff), messageInputField.selectionStart) + const updatedPrefixChars = prefixChars.replace(unwrapWith, '') + const postfixChars = messageInputField.getText(messageInputField.selectionEnd, (messageInputField.selectionEnd + selectTextDiff)) + const updatedPostfixChars = postfixChars.replace(unwrapWith, '') - // Create updated selected string with pre and post formatting characters - const updatedSelectedStringWithFormatChars = updatedPrefixChars + messageInputField.selectedText + updatedPostfixChars + // Create updated selected string with pre and post formatting characters + const updatedSelectedStringWithFormatChars = updatedPrefixChars + messageInputField.selectedText + updatedPostfixChars - messageInputField.remove(messageInputField.selectionStart - selectTextDiff, messageInputField.selectionEnd + selectTextDiff) + messageInputField.remove(messageInputField.selectionStart - selectTextDiff, messageInputField.selectionEnd + selectTextDiff) - insertInTextInput(messageInputField.selectionStart, updatedSelectedStringWithFormatChars) + insertInTextInput(messageInputField.selectionStart, updatedSelectedStringWithFormatChars) - messageInputField.select(newSelectionStart, newSelectionEnd) + messageInputField.select(newSelectionStart, newSelectionEnd) } function getPlainText() { @@ -407,7 +433,7 @@ Rectangle { id: dummyContactList model: control.usersStore ? control.usersStore.usersModel : [] delegate: Item { - property string contactName: model.name + property string contactName: model.name || "" } } @@ -652,7 +678,6 @@ Rectangle { ] onAccepted: { imageBtn.highlighted = false - imageBtn2.highlighted = false let validImages = validateImages(imageDialog.fileUrls) if (validImages.length > 0) { control.showImageArea(validImages) @@ -661,7 +686,6 @@ Rectangle { } onRejected: { imageBtn.highlighted = false - imageBtn2.highlighted = false } } @@ -766,505 +790,428 @@ Rectangle { } } - StatusQ.StatusFlatRoundButton { - id: chatCommandsBtn - width: 32 - height: 32 - anchors.left: parent.left - anchors.leftMargin: 4 - anchors.bottom: parent.bottom - anchors.bottomMargin: 16 - icon.name: "chat-commands" - type: StatusQ.StatusFlatRoundButton.Type.Tertiary - visible: RootStore.isWalletEnabled && !isEdit && control.chatType === Constants.chatType.oneToOne && !control.isStatusUpdateInput - enabled: !control.isContactBlocked - onClicked: { - chatCommandsPopup.opened ? - chatCommandsPopup.close() : - chatCommandsPopup.open() - } - } - - StatusQ.StatusFlatRoundButton { - id: imageBtn - width: 32 - height: 32 - anchors.left: chatCommandsBtn.visible ? chatCommandsBtn.right : parent.left - anchors.leftMargin: chatCommandsBtn.visible ? 2 : 4 - anchors.bottom: parent.bottom - anchors.bottomMargin: 16 - icon.name: "image" - type: StatusQ.StatusFlatRoundButton.Type.Tertiary - visible: !isEdit && control.chatType !== Constants.chatType.publicChat && !control.isStatusUpdateInput - enabled: !control.isContactBlocked - onClicked: { - highlighted = true - imageDialog.open() - } - } - - Rectangle { - id: messageInput - enabled: !control.isContactBlocked - property int maxInputFieldHeight: control.isStatusUpdateInput ? 124 : 112 - property int defaultInputFieldHeight: control.isStatusUpdateInput ? 56 : 40 - anchors.left: imageBtn.visible ? imageBtn.right : parent.left - anchors.leftMargin: imageBtn.visible ? 5 : Style.current.smallPadding - anchors.top: control.isStatusUpdateInput ? parent.top : undefined - anchors.bottom: !control.isStatusUpdateInput ? parent.bottom : undefined - anchors.bottomMargin: control.isStatusUpdateInput ? 0 : 12 - anchors.right: unblockBtn.visible ? unblockBtn.left : parent.right - anchors.rightMargin: Style.current.smallPadding - height: { - if (messageInputField.implicitHeight <= messageInput.defaultInputFieldHeight) { - return messageInput.defaultInputFieldHeight + RowLayout { + id: layout + anchors.fill: parent + spacing: 4 + + StatusQ.StatusFlatRoundButton { + id: chatCommandsBtn + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignBottom + Layout.bottomMargin: 4 + icon.name: "chat-commands" + type: StatusQ.StatusFlatRoundButton.Type.Tertiary + visible: RootStore.isWalletEnabled && !isEdit && control.chatType === Constants.chatType.oneToOne + enabled: !control.isContactBlocked + onClicked: { + chatCommandsPopup.opened ? + chatCommandsPopup.close() : + chatCommandsPopup.open() } - if (messageInputField.implicitHeight >= messageInput.maxInputFieldHeight) { - return messageInput.maxInputFieldHeight - } - return messageInputField.implicitHeight } - color: isEdit ? Theme.palette.statusChatInput.secondaryBackgroundColor : Style.current.inputBackground - radius: control.isStatusUpdateInput ? 36 : - height > defaultInputFieldHeight + 1 || extendedArea.visible ? 16 : 32 - - ColumnLayout { - id: validators - anchors.bottom: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? extendedArea.top : undefined - anchors.bottomMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? -4 : undefined - anchors.top: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? extendedArea.bottom : undefined - anchors.topMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? (isImage ? -4 : 4) : undefined - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - z: 1 - StatusChatImageExtensionValidator { - Layout.alignment: Qt.AlignHCenter - } - StatusChatImageSizeValidator { - Layout.alignment: Qt.AlignHCenter - } - StatusChatImageQtyValidator { - Layout.alignment: Qt.AlignHCenter + StatusQ.StatusFlatRoundButton { + id: imageBtn + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignBottom + Layout.bottomMargin: 4 + icon.name: "image" + type: StatusQ.StatusFlatRoundButton.Type.Tertiary + visible: !isEdit && control.chatType !== Constants.chatType.publicChat + enabled: !control.isContactBlocked + onClicked: { + highlighted = true + imageDialog.open() } } Rectangle { - id: extendedArea - visible: isImage || isReply - height: { - if (visible) { - if (isImage) { - return imageArea.height - } + id: messageInput - if (isReply) { - return replyArea.height + replyArea.anchors.topMargin - } + readonly property int defaultInputFieldHeight: 40 + + Layout.fillWidth: true + + + implicitHeight: inputLayout.implicitHeight + inputLayout.anchors.topMargin + inputLayout.anchors.bottomMargin + implicitWidth: inputLayout.implicitWidth + inputLayout.anchors.leftMargin + inputLayout.anchors.rightMargin + + enabled: !control.isContactBlocked + color: isEdit ? Theme.palette.statusChatInput.secondaryBackgroundColor : Style.current.inputBackground + radius: 32 + + ColumnLayout { + id: validators + anchors.bottom: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? parent.top : undefined + anchors.bottomMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? -4 : undefined + anchors.top: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? parent.bottom : undefined + anchors.topMargin: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Bottom ? (isImage ? -4 : 4) : undefined + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + z: 1 + + StatusChatImageExtensionValidator { + Layout.alignment: Qt.AlignHCenter + } + StatusChatImageSizeValidator { + Layout.alignment: Qt.AlignHCenter + } + StatusChatImageQtyValidator { + Layout.alignment: Qt.AlignHCenter } - return 0 } - anchors.left: messageInput.left - anchors.right: messageInput.right - anchors.bottom: control.isStatusUpdateInput ? undefined : messageInput.top - anchors.top: control.isStatusUpdateInput ? messageInput.bottom : undefined - anchors.topMargin: control.isStatusUpdateInput ? -Style.current.halfPadding : 0 - color: isEdit ? Style.current.secondaryInputBackground : Style.current.inputBackground - radius: control.isStatusUpdateInput ? 36 : 16 Rectangle { + // Bottom right corner has different radius color: parent.color + anchors.bottom: parent.bottom anchors.right: parent.right - anchors.left: parent.left - height: control.isStatusUpdateInput ? 64 : 30 - anchors.top: control.isStatusUpdateInput ? parent.top : undefined - anchors.topMargin: control.isStatusUpdateInput ? -24 : 0 - anchors.bottom: control.isStatusUpdateInput ? undefined : parent.bottom - anchors.bottomMargin: control.isStatusUpdateInput ? 0 : -height/2 + height: parent.height / 2 + width: 32 + radius: Style.current.radius } - StatusChatInputImageArea { - id: imageArea - anchors.left: parent.left - anchors.leftMargin: control.isStatusUpdateInput ? profileImage.width + Style.current.padding : Style.current.halfPadding - anchors.right: parent.right - anchors.rightMargin: control.isStatusUpdateInput ? actions.width + 2* Style.current.padding : Style.current.halfPadding - anchors.top: parent.top - anchors.topMargin: Style.current.halfPadding - visible: isImage - width: messageInputField.width - actions.width - onImageClicked: Global.openImagePopup(chatImage, messageContextMenu) - onImageRemoved: { - if (control.fileUrls.length > index && control.fileUrls[index]) { - control.fileUrls.splice(index, 1) + ColumnLayout { + id: inputLayout + + anchors.fill: parent + spacing: 4 + + StatusChatInputReplyArea { + id: replyArea + visible: isReply + Layout.fillWidth: true + Layout.margins: 2 + onCloseButtonClicked: { + isReply = false } - isImage = control.fileUrls.length > 0 - validateImages(control.fileUrls) } - } - StatusChatInputReplyArea { - id: replyArea - visible: isReply - anchors.left: parent.left - anchors.leftMargin: 2 - anchors.right: parent.right - anchors.rightMargin: 2 - anchors.top: parent.top - anchors.topMargin: 2 - // Not Refactored Yet -// stickerData: sticker - onCloseButtonClicked: { - isReply = false + StatusChatInputImageArea { + id: imageArea + Layout.fillWidth: true + Layout.leftMargin: Style.current.halfPadding + Layout.rightMargin: Style.current.halfPadding + visible: isImage + onImageClicked: Global.openImagePopup(chatImage, messageContextMenu) + onImageRemoved: { + if (control.fileUrls.length > index && control.fileUrls[index]) { + control.fileUrls.splice(index, 1) + } + isImage = control.fileUrls.length > 0 + validateImages(control.fileUrls) + } } - } - } - StatusSmartIdenticon { - id: profileImage - anchors.left: parent.left - anchors.leftMargin: Style.current.smallPadding - anchors.top: parent.top - anchors.topMargin: Style.current.halfPadding - image.source: userProfile.icon - visible: control.isStatusUpdateInput - } + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.current.radius + + StatusScrollView { + id: inputScrollView + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: Style.current.halfPadding + Layout.rightMargin: Style.current.halfPadding + Layout.maximumHeight: 112 + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + padding: 0 + + TextArea { + id: messageInputField + + property var lastClick: 0 + property int cursorWhenPressed: 0 + + width: inputScrollView.availableWidth + + textFormat: Text.RichText + font.pixelSize: 15 + font.family: Style.current.fontRegular.name + wrapMode: TextArea.Wrap + placeholderText: control.chatInputPlaceholder + placeholderTextColor: Style.current.secondaryText + selectByMouse: true + color: isEdit ? Theme.palette.directColor1 : Style.current.textColor + topPadding: Style.current.smallPadding + bottomPadding: 12 + leftPadding: 0 + padding: 0 + Keys.onPressed: { + keyEvent = event; + onKeyPress(event) + cursorWhenPressed = cursorPosition; + } + Keys.onReleased: onRelease(event) // gives much more up to date cursorPosition + Keys.onShortcutOverride: event.accepted = isUploadFilePressed(event) + selectionColor: Style.current.primarySelectionColor + persistentSelection: true + property var keyEvent + onCursorPositionChanged: { + if (mentionsPos.length > 0) { + for (var i = 0; i < mentionsPos.length; i++) { + if ((messageInputField.cursorPosition === (mentionsPos[i].leftIndex + 1)) && (keyEvent.key === Qt.Key_Right)) { + messageInputField.cursorPosition = mentionsPos[i].rightIndex; + } else if (messageInputField.cursorPosition === (mentionsPos[i].rightIndex - 1)) { + if (keyEvent.key === Qt.Key_Left) { + messageInputField.cursorPosition = mentionsPos[i].leftIndex; + } else if ((keyEvent.key === Qt.Key_Backspace) || (keyEvent.key === Qt.Key_Delete)) { + messageInputField.remove(mentionsPos[i].rightIndex, mentionsPos[i].leftIndex); + mentionsPos.pop(i); + } + } else if (((messageInputField.cursorPosition > mentionsPos[i].leftIndex) && + (messageInputField.cursorPosition < mentionsPos[i].rightIndex)) && + ((keyEvent.key === Qt.Key_Left) && ((keyEvent.modifiers & Qt.AltModifier) || + (keyEvent.modifiers & Qt.ControlModifier)))) { + messageInputField.cursorPosition = mentionsPos[i].leftIndex; + } else if ((keyEvent.key === Qt.Key_Up) || (keyEvent.key === Qt.Key_Down)) { + if (messageInputField.cursorPosition >= mentionsPos[i].leftIndex && + messageInputField.cursorPosition <= (((mentionsPos[i].leftIndex + mentionsPos[i].rightIndex)/2))) { + messageInputField.cursorPosition = mentionsPos[i].leftIndex; + } else if (messageInputField.cursorPosition <= mentionsPos[i].rightIndex && + messageInputField.cursorPosition > (((mentionsPos[i].leftIndex + mentionsPos[i].rightIndex)/2))) { + messageInputField.cursorPosition = mentionsPos[i].rightIndex; + } + } + } + } + if ((mentionsPos.length > 0) && (cursorPosition < length) && getText(cursorPosition, length).includes("@") + && (keyEvent.key !== Qt.Key_Right) && (keyEvent.key !== Qt.Key_Left) && (keyEvent.key !== Qt.Key_Up) + && (keyEvent.key !== Qt.Key_Down)) { + var unformattedText = getText(cursorPosition, length); + for (var k = 0; k < mentionsPos.length; k++) { + if ((unformattedText.indexOf(mentionsPos[k].name) !== -1) && (unformattedText.indexOf(mentionsPos[k].name) !== mentionsPos[k].leftIndex)) { + mentionsPos[k].leftIndex = (cursorPosition + unformattedText.indexOf(mentionsPos[k].name) - 1); + mentionsPos[k].rightIndex = (cursorPosition + unformattedText.indexOf(mentionsPos[k].name) + mentionsPos[k].name.length); + } + } + } + } + onTextChanged: { + if (length <= control.messageLimit) { + var symbols = ":='xX><0O;*dB8-D#%\\"; + if ((length > 1) && (symbols.indexOf(getText((cursorPosition - 2), (cursorPosition - 1))) !== -1) + && (!getText((cursorPosition - 7), cursorPosition).includes("http"))) { + const emojis = EmojiJSON.emoji_json.filter(function (emoji) { + if (emoji.aliases_ascii.includes(getText((cursorPosition - 2), cursorPosition)) || + emoji.aliases_ascii.includes(getText((cursorPosition - 3), cursorPosition))) { + var has2Chars = emoji.aliases_ascii.includes(getText((cursorPosition - 2), cursorPosition)); + replaceWithEmoji("", getText(cursorPosition - (has2Chars ? 2 : 3), cursorPosition), emoji.unicode); + } + }) + } + if (text === "") { + mentionsPos = []; + } + } else { + var removeFrom = (cursorPosition < messageLimit) ? cursorWhenPressed : messageLimit; + remove(removeFrom, cursorPosition); + } + messageLengthLimit.remainingChars = (messageLimit - length); + } - StatusScrollView { - id: scrollView - padding: 0 - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: profileImage.visible ? profileImage.right : parent.left - anchors.leftMargin: Style.current.smallPadding - anchors.right: actions.left - anchors.rightMargin: Style.current.halfPadding - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - - TextArea { - id: messageInputField - property var lastClick: 0 - property int cursorWhenPressed: 0 - width: scrollView.availableWidth - textFormat: Text.RichText - font.pixelSize: 15 - font.family: Style.current.fontRegular.name - wrapMode: TextArea.Wrap - placeholderText: control.chatInputPlaceholder - placeholderTextColor: Style.current.secondaryText - selectByMouse: true - color: isEdit ? Theme.palette.directColor1 : Style.current.textColor - topPadding: control.isStatusUpdateInput ? 18 : Style.current.smallPadding - bottomPadding: control.isStatusUpdateInput ? 14 : 12 - Keys.onPressed: { - keyEvent = event; - onKeyPress(event) - cursorWhenPressed = cursorPosition; - } - Keys.onReleased: onRelease(event) // gives much more up to date cursorPosition - Keys.onShortcutOverride: event.accepted = isUploadFilePressed(event) - leftPadding: 0 - selectionColor: Style.current.primarySelectionColor - persistentSelection: true - property var keyEvent - onCursorPositionChanged: { - if (mentionsPos.length > 0) { - for (var i = 0; i < mentionsPos.length; i++) { - if ((messageInputField.cursorPosition === (mentionsPos[i].leftIndex + 1)) && (keyEvent.key === Qt.Key_Right)) { - messageInputField.cursorPosition = mentionsPos[i].rightIndex; - } else if (messageInputField.cursorPosition === (mentionsPos[i].rightIndex - 1)) { - if (keyEvent.key === Qt.Key_Left) { - messageInputField.cursorPosition = mentionsPos[i].leftIndex; - } else if ((keyEvent.key === Qt.Key_Backspace) || (keyEvent.key === Qt.Key_Delete)) { - messageInputField.remove(mentionsPos[i].rightIndex, mentionsPos[i].leftIndex); - mentionsPos.pop(i); + onReleased: function (event) { + const now = Date.now() + if (messageInputField.selectedText.trim() !== "") { + // If it's a double click, just check the mouse position + // If it's a mouse select, use the start and end position average) + let x = now < messageInputField.lastClick + 500 ? x = event.x : + (messageInputField.cursorRectangle.x + event.x) / 2 + x -= textFormatMenu.width / 2 + + textFormatMenu.popup(x, -messageInputField.height-2) + messageInputField.forceActiveFocus(); } - } else if (((messageInputField.cursorPosition > mentionsPos[i].leftIndex) && - (messageInputField.cursorPosition < mentionsPos[i].rightIndex)) && - ((keyEvent.key === Qt.Key_Left) && ((keyEvent.modifiers & Qt.AltModifier) || - (keyEvent.modifiers & Qt.ControlModifier)))) { - messageInputField.cursorPosition = mentionsPos[i].leftIndex; - } else if ((keyEvent.key === Qt.Key_Up) || (keyEvent.key === Qt.Key_Down)) { - if (messageInputField.cursorPosition >= mentionsPos[i].leftIndex && - messageInputField.cursorPosition <= (((mentionsPos[i].leftIndex + mentionsPos[i].rightIndex)/2))) { - messageInputField.cursorPosition = mentionsPos[i].leftIndex; - } else if (messageInputField.cursorPosition <= mentionsPos[i].rightIndex && - messageInputField.cursorPosition > (((mentionsPos[i].leftIndex + mentionsPos[i].rightIndex)/2))) { - messageInputField.cursorPosition = mentionsPos[i].rightIndex; + lastClick = now + } + + cursorDelegate: Rectangle { + color: Theme.palette.primaryColor1 + implicitWidth: 2 + implicitHeight: 22 + radius: 1 + visible: messageInputField.cursorVisible + + SequentialAnimation on visible { + loops: Animation.Infinite + running: messageInputField.cursorVisible + PropertyAnimation { to: false; duration: 600; } + PropertyAnimation { to: true; duration: 600; } } } - } - } - if ((mentionsPos.length > 0) && (cursorPosition < length) && getText(cursorPosition, length).includes("@") - && (keyEvent.key !== Qt.Key_Right) && (keyEvent.key !== Qt.Key_Left) && (keyEvent.key !== Qt.Key_Up) - && (keyEvent.key !== Qt.Key_Down)) { - var unformattedText = getText(cursorPosition, length); - for (var k = 0; k < mentionsPos.length; k++) { - if ((unformattedText.indexOf(mentionsPos[k].name) !== -1) && (unformattedText.indexOf(mentionsPos[k].name) !== mentionsPos[k].leftIndex)) { - mentionsPos[k].leftIndex = (cursorPosition + unformattedText.indexOf(mentionsPos[k].name) - 1); - mentionsPos[k].rightIndex = (cursorPosition + unformattedText.indexOf(mentionsPos[k].name) + mentionsPos[k].name.length); + + StatusSyntaxHighlighter { + quickTextDocument: messageInputField.textDocument } - } - } - } - onTextChanged: { - if (length <= control.messageLimit) { - var symbols = ":='xX><0O;*dB8-D#%\\"; - if ((length > 1) && (symbols.indexOf(getText((cursorPosition - 2), (cursorPosition - 1))) !== -1) - && (!getText((cursorPosition - 7), cursorPosition).includes("http"))) { - const emojis = EmojiJSON.emoji_json.filter(function (emoji) { - if (emoji.aliases_ascii.includes(getText((cursorPosition - 2), cursorPosition)) || - emoji.aliases_ascii.includes(getText((cursorPosition - 3), cursorPosition))) { - var has2Chars = emoji.aliases_ascii.includes(getText((cursorPosition - 2), cursorPosition)); - replaceWithEmoji("", getText(cursorPosition - (has2Chars ? 2 : 3), cursorPosition), emoji.unicode); + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + enabled: parent.hoveredLink + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor + } + StatusTextFormatMenu { + id: textFormatMenu + + StatusChatInputTextFormationAction { + wrapper: "**" + icon.name: "bold" + text: qsTr("Bold") + selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) + onActionTriggered: checked ? + unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : + wrapSelection(wrapper) } - }) - } - if (text === "") { - mentionsPos = []; + StatusChatInputTextFormationAction { + wrapper: "*" + icon.name: "italic" + text: qsTr("Italic") + selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) + checked: (surroundedBy("*") && !surroundedBy("**")) || surroundedBy("***") + onActionTriggered: checked ? + unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : + wrapSelection(wrapper) + } + StatusChatInputTextFormationAction { + wrapper: "~~" + icon.name: "strikethrough" + text: qsTr("Strikethrough") + selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) + onActionTriggered: checked ? + unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : + wrapSelection(wrapper) + } + StatusChatInputTextFormationAction { + wrapper: "`" + icon.name: "code" + text: qsTr("Code") + selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) + onActionTriggered: checked ? + unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : + wrapSelection(wrapper) + } + onClosed: { + messageInputField.deselect(); + } + } } - } else { - var removeFrom = (cursorPosition < messageLimit) ? cursorWhenPressed : messageLimit; - remove(removeFrom, cursorPosition); - } - messageLengthLimit.remainingChars = (messageLimit - length); - } - - onReleased: function (event) { - const now = Date.now() - if (messageInputField.selectedText.trim() !== "") { - // If it's a double click, just check the mouse position - // If it's a mouse select, use the start and end position average) - let x = now < messageInputField.lastClick + 500 ? x = event.x : - (messageInputField.cursorRectangle.x + event.x) / 2 - x -= textFormatMenu.width / 2 - - textFormatMenu.popup(x, -messageInputField.height-2) - messageInputField.forceActiveFocus(); - } - lastClick = now - } - StatusSyntaxHighlighter { - quickTextDocument: messageInputField.textDocument - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - enabled: parent.hoveredLink - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor - } - StatusTextFormatMenu { - id: textFormatMenu - - StatusChatInputTextFormationAction { - wrapper: "**" - icon.name: "bold" - text: qsTr("Bold") - selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) - onActionTriggered: checked ? - unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : - wrapSelection(wrapper) - } - StatusChatInputTextFormationAction { - wrapper: "*" - icon.name: "italic" - text: qsTr("Italic") - selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) - checked: (surroundedBy("*") && !surroundedBy("**")) || surroundedBy("***") - onActionTriggered: checked ? - unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : - wrapSelection(wrapper) - } - StatusChatInputTextFormationAction { - wrapper: "~~" - icon.name: "strikethrough" - text: qsTr("Strikethrough") - selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) - onActionTriggered: checked ? - unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : - wrapSelection(wrapper) - } - StatusChatInputTextFormationAction { - wrapper: "`" - icon.name: "code" - text: qsTr("Code") - selectedTextWithFormationChars: RootStore.getSelectedTextWithFormationChars(messageInputField) - onActionTriggered: checked ? - unwrapSelection(wrapper, RootStore.getSelectedTextWithFormationChars(messageInputField)) : - wrapSelection(wrapper) - } - onClosed: { - messageInputField.deselect(); + Shortcut { + enabled: messageInputField.activeFocus + sequence: StandardKey.Bold + onActivated: wrapSelection("**") + } + Shortcut { + enabled: messageInputField.activeFocus + sequence: StandardKey.Italic + onActivated: wrapSelection("*") + } + Shortcut { + enabled: messageInputField.activeFocus + sequence: "Ctrl+Shift+Alt+C" + onActivated: wrapSelection("```") + } + Shortcut { + enabled: messageInputField.activeFocus + sequence: "Ctrl+Shift+C" + onActivated: wrapSelection("`") + } + Shortcut { + enabled: messageInputField.activeFocus + sequence: "Ctrl+Alt+-" + onActivated: wrapSelection("~~") + } + Shortcut { + enabled: messageInputField.activeFocus + sequence: "Ctrl+Shift+X" + onActivated: wrapSelection("~~") + } + Shortcut { + enabled: messageInputField.activeFocus + sequence: "Ctrl+Meta+Space" + onActivated: emojiBtn.clicked(null) + } } - } - } - - Shortcut { - enabled: messageInputField.activeFocus - sequence: StandardKey.Bold - onActivated: wrapSelection("**") - } - Shortcut { - enabled: messageInputField.activeFocus - sequence: StandardKey.Italic - onActivated: wrapSelection("*") - } - Shortcut { - enabled: messageInputField.activeFocus - sequence: "Ctrl+Shift+Alt+C" - onActivated: wrapSelection("```") - } - Shortcut { - enabled: messageInputField.activeFocus - sequence: "Ctrl+Shift+C" - onActivated: wrapSelection("`") - } - Shortcut { - enabled: messageInputField.activeFocus - sequence: "Ctrl+Alt+-" - onActivated: wrapSelection("~~") - } - Shortcut { - enabled: messageInputField.activeFocus - sequence: "Ctrl+Shift+X" - onActivated: wrapSelection("~~") - } - Shortcut { - enabled: messageInputField.activeFocus - sequence: "Ctrl+Meta+Space" - onActivated: emojiBtn.clicked(null) - } - - } - - Rectangle { - color: parent.color - anchors.bottom: parent.bottom - anchors.right: parent.right - visible: !control.isStatusUpdateInput - height: parent.height / 2 - width: 32 - radius: Style.current.radius - } - - StyledText { - id: messageLengthLimit - property int remainingChars: -1 - anchors.right: parent.right - anchors.bottom: actions.top - anchors.rightMargin: control.isStatusUpdateInput ? Style.current.padding : Style.current.radius - leftPadding: Style.current.halfPadding - rightPadding: Style.current.halfPadding - visible: ((messageInputField.length >= control.messageLimitVisible) && (messageInputField.length <= control.messageLimit)) - color: (remainingChars <= messageLimitVisible) ? Style.current.danger : Style.current.textColor - text: visible ? remainingChars.toString() : "" - } - Item { - id: actions - width: control.isStatusUpdateInput ? - imageBtn2.width + sendBtn.anchors.leftMargin + sendBtn.width : - emojiBtn.width + stickersBtn.anchors.leftMargin + stickersBtn.width - anchors.bottom: control.isStatusUpdateInput && extendedArea.visible ? extendedArea.bottom : parent.bottom - anchors.bottomMargin: control.isStatusUpdateInput ? Style.current.smallPadding+2: 4 - anchors.right: parent.right - anchors.rightMargin: Style.current.radius - height: emojiBtn.height - - StatusQ.StatusFlatRoundButton { - id: imageBtn2 - implicitHeight: 32 - implicitWidth: 32 - anchors.right: sendBtn.left - anchors.rightMargin: 2 - anchors.bottom: parent.bottom - icon.name: "image" - type: StatusQ.StatusFlatRoundButton.Type.Tertiary - visible: control.isStatusUpdateInput + Column { + Layout.alignment: Qt.AlignBottom + Layout.bottomMargin: 6 + + StyledText { + id: messageLengthLimit + property int remainingChars: -1 + leftPadding: Style.current.halfPadding + rightPadding: Style.current.halfPadding + visible: ((messageInputField.length >= control.messageLimitVisible) && (messageInputField.length <= control.messageLimit)) + color: (remainingChars <= messageLimitVisible) ? Style.current.danger : Style.current.textColor + text: visible ? remainingChars.toString() : "" + } - onClicked: { - highlighted = true - imageDialog.open() - } - } + Row { + id: actions + spacing: 2 + + StatusQ.StatusFlatRoundButton { + id: emojiBtn + enabled: !control.emojiPopupOpened + implicitHeight: 32 + implicitWidth: 32 + icon.name: "emojis" + type: StatusQ.StatusFlatRoundButton.Type.Tertiary + color: "transparent" + onClicked: { + control.emojiPopupOpened = true + togglePopup(emojiPopup, emojiBtn) + emojiPopup.x = Global.applicationWindow.width - emojiPopup.width - Style.current.halfPadding + emojiPopup.y = Global.applicationWindow.height - emojiPopup.height - control.height + } + } - StatusQ.StatusFlatButton { - id: sendBtn - icon.name: "send" - text: qsTr("Send") - size: StatusQ.StatusBaseButton.Size.Small - anchors.right: parent.right - anchors.rightMargin: Style.current.halfPadding - anchors.verticalCenter: parent.verticalCenter - visible: imageBtn2.visible - enabled: (globalUtils.plainText(StatusQUtils.Emoji.deparse(messageInputField.text)).length > 0 || isImage) && messageInputField.length < messageLimit - onClicked: function (event) { - control.sendMessage(event) - control.hideExtendedArea(); - } - } + StatusQ.StatusFlatRoundButton { + id: gifBtn + implicitHeight: 32 + implicitWidth: 32 + visible: !isEdit && RootStore.isGifWidgetEnabled + icon.name: "gif" + type: StatusQ.StatusFlatRoundButton.Type.Tertiary + color: "transparent" + onClicked: togglePopup(gifPopup, gifBtn) + } - StatusQ.StatusFlatRoundButton { - id: emojiBtn - enabled: !control.emojiPopupOpened - implicitHeight: 32 - implicitWidth: 32 - anchors.left: parent.left - anchors.bottom: parent.bottom - visible: !imageBtn2.visible - icon.name: "emojis" - type: StatusQ.StatusFlatRoundButton.Type.Tertiary - color: "transparent" - onClicked: { - control.emojiPopupOpened = true - togglePopup(emojiPopup, emojiBtn) - emojiPopup.x = Global.applicationWindow.width - emojiPopup.width - Style.current.halfPadding - emojiPopup.y = Global.applicationWindow.height - emojiPopup.height - control.height + StatusQ.StatusFlatRoundButton { + id: stickersBtn + implicitHeight: 32 + implicitWidth: 32 + width: visible ? 32 : 0 + icon.name: "stickers" + type: StatusQ.StatusFlatRoundButton.Type.Tertiary + visible: !isEdit && emojiBtn.visible + color: "transparent" + onClicked: togglePopup(stickersPopup, stickersBtn) + } + } + } } } - StatusQ.StatusFlatRoundButton { - id: gifBtn - implicitHeight: 32 - implicitWidth: 32 - anchors.right: emojiBtn.left - anchors.rightMargin: 2 - anchors.bottom: parent.bottom - visible: !isEdit && RootStore.isGifWidgetEnabled - icon.name: "gif" - type: StatusQ.StatusFlatRoundButton.Type.Tertiary - color: "transparent" - onClicked: togglePopup(gifPopup, gifBtn) - } + } - StatusQ.StatusFlatRoundButton { - id: stickersBtn - implicitHeight: 32 - implicitWidth: 32 - width: visible ? 32 : 0 - anchors.left: emojiBtn.right - anchors.leftMargin: 2 - anchors.bottom: parent.bottom - icon.name: "stickers" - type: StatusQ.StatusFlatRoundButton.Type.Tertiary - visible: !isEdit && emojiBtn.visible - color: "transparent" - onClicked: togglePopup(stickersPopup, stickersBtn) + StatusQ.StatusButton { + id: unblockBtn + Layout.alignment: Qt.AlignBottom + Layout.bottomMargin: 4 + visible: control.isContactBlocked + text: qsTr("Unblock") + type: StatusQ.StatusBaseButton.Type.Danger + onClicked: function (event) { + control.unblockChat() } } } - StatusQ.StatusButton { - id: unblockBtn - visible: control.isContactBlocked - anchors.right: parent.right - anchors.rightMargin: Style.current.halfPadding - anchors.bottom: messageInput.bottom - text: qsTr("Unblock") - type: StatusQ.StatusBaseButton.Type.Danger - onClicked: function (event) { - control.unblockChat() - } - } } diff --git a/ui/imports/shared/status/StatusChatInputReplyArea.qml b/ui/imports/shared/status/StatusChatInputReplyArea.qml index 5436d8162ea..13fe7a5ba0b 100644 --- a/ui/imports/shared/status/StatusChatInputReplyArea.qml +++ b/ui/imports/shared/status/StatusChatInputReplyArea.qml @@ -10,10 +10,12 @@ import StatusQ.Core.Utils 0.1 as StatusQUtils Rectangle { id: root - height: (root.contentType === Constants.messageContentType.imageType) ? - replyToUsername.height + imageThumbnail.height + Style.current.padding : - (root.contentType === Constants.messageContentType.stickerType) ? - replyToUsername.height + stickerThumbnail.height + Style.current.padding : 50 + implicitHeight: (root.contentType === Constants.messageContentType.imageType) + ? replyToUsername.height + imageThumbnail.height + Style.current.padding + : (root.contentType === Constants.messageContentType.stickerType) + ? replyToUsername.height + stickerThumbnail.height + Style.current.padding + : 50 + color: Style.current.replyBackground radius: 16 clip: true @@ -61,7 +63,7 @@ Rectangle { StyledText { id: replyText - text: Utils.getMessageWithStyle(StatusQUtils.Emoji.parse(Utils.linkifyAndXSS(message)), false) + text: Utils.getMessageWithStyle(StatusQUtils.Emoji.parse(StatusQUtils.Utils.linkifyAndXSS(message)), false) anchors.fill: parent elide: Text.ElideRight font.pixelSize: 13 diff --git a/ui/imports/shared/status/StatusGifPopup.qml b/ui/imports/shared/status/StatusGifPopup.qml index e5aa4db8d38..9bf8cedb27f 100644 --- a/ui/imports/shared/status/StatusGifPopup.qml +++ b/ui/imports/shared/status/StatusGifPopup.qml @@ -213,10 +213,10 @@ Popup { anchors.fill: parent spacing: 12 - SVGImage { + StatusIcon { id: gifImage anchors.horizontalCenter: parent.horizontalCenter - source: Style.svg(`gif-${Style.current.name}`) + icon: "gif" } StyledText { diff --git a/ui/imports/shared/status/StatusStickersPopup.qml b/ui/imports/shared/status/StatusStickersPopup.qml index a6af210e478..54faeedc82c 100644 --- a/ui/imports/shared/status/StatusStickersPopup.qml +++ b/ui/imports/shared/status/StatusStickersPopup.qml @@ -236,7 +236,7 @@ Popup { RowLayout { id: stickersRowLayout - width: scrollView.availableWidth + width: inputScrollView.availableWidth spacing: Style.current.padding Repeater { diff --git a/ui/imports/shared/views/ProfileView.qml b/ui/imports/shared/views/ProfileView.qml index 4727cc14fd5..476fcf93e47 100644 --- a/ui/imports/shared/views/ProfileView.qml +++ b/ui/imports/shared/views/ProfileView.qml @@ -356,7 +356,7 @@ Rectangle { messageTimestamp: root.verificationRequestedAt senderDisplayName: userProfile.name senderIcon: userProfile.icon - message: root.verificationChallenge + messageText: root.verificationChallenge messageContentType: Constants.messageContentType.messageType placeholderMessage: true } @@ -364,13 +364,13 @@ Rectangle { MessageView { id: responseMessage visible: root.showVerificationPendingSection && !!root.verificationResponse - width: parent.width + Layout.fillWidth: true isMessage: true shouldRepeatHeader: true messageTimestamp: root.verificationRepliedAt senderDisplayName: root.verificationResponseDisplayName senderIcon: root.verificationResponseIcon - message: root.verificationResponse + messageText: root.verificationResponse messageContentType: Constants.messageContentType.messageType placeholderMessage: true } diff --git a/ui/imports/shared/views/chat/ChatTextView.qml b/ui/imports/shared/views/chat/ChatTextView.qml index b8ff3a34010..2ea0cae626d 100644 --- a/ui/imports/shared/views/chat/ChatTextView.qml +++ b/ui/imports/shared/views/chat/ChatTextView.qml @@ -113,16 +113,16 @@ Item { } text: { - if (contentType === Constants.messageContentType.stickerType) return ""; - let msg = Utils.linkifyAndXSS(message); + if (contentType === Constants.messageContentType.stickerType) + return ""; + let msg = StatusQUtils.Utils.linkifyAndXSS(message); if (isEmoji) return StatusQUtils.Emoji.parse(msg, StatusQUtils.Emoji.size.middle, StatusQUtils.Emoji.format.png); if (isEdited) { let index = msg.endsWith("code>") ? msg.length : msg.length - 4 - return Utils.getMessageWithStyle(StatusQUtils.Emoji.parse(msg.slice(0, index) + Constants.editLabel + msg.slice(index)), isCurrentUser, hoveredLink) + return StatusQUtils.Utils.getMessageWithStyle(StatusQUtils.Emoji.parse(msg.slice(0, index) + Constants.editLabel + msg.slice(index)), isCurrentUser, hoveredLink) } - return Utils.getMessageWithStyle(StatusQUtils.Emoji.parse(msg), isCurrentUser, hoveredLink) - + return StatusQUtils.Utils.getMessageWithStyle(StatusQUtils.Emoji.parse(msg), isCurrentUser, hoveredLink) } } diff --git a/ui/imports/shared/views/chat/CompactMessageView.qml b/ui/imports/shared/views/chat/CompactMessageView.qml deleted file mode 100644 index a9a32f5d0ca..00000000000 --- a/ui/imports/shared/views/chat/CompactMessageView.qml +++ /dev/null @@ -1,798 +0,0 @@ -import QtQuick 2.13 -import QtGraphicalEffects 1.13 - -import utils 1.0 -import shared.panels 1.0 -import shared.status 1.0 -import shared.controls 1.0 -import shared.panels.chat 1.0 -import shared.views.chat 1.0 -import shared.controls.chat 1.0 - -import StatusQ.Controls 0.1 as StatusQControls -import StatusQ.Core.Utils 0.1 as StatusQUtils -import StatusQ.Components 0.1 - -Item { - id: root - - property var store - property var messageStore - property var usersStore - property var contactsStore - - property var chatLogView - property var emojiPopup - - property var messageContextMenu - property var container - property int contentType - property bool isChatBlocked: false - property bool isActiveChannel: false - property int senderTrustStatus - - property int chatHorizontalPadding: Style.current.halfPadding - property int chatVerticalPadding: 7 - property bool headerRepeatCondition: (authorCurrentMsg !== authorPrevMsg || - shouldRepeatHeader || dateGroupLbl.visible || chatReply.active) - property bool stickersLoaded: false - property string sticker - property int stickerPack - property bool isMessageActive: false - property bool amISender: false - property string senderIcon: "" - property bool isHovered: false - property bool isInPinnedPopup: false - property bool pinnedMessage: false - property bool canPin: false - property string communityId - property bool editModeOn: false - property string linkUrls: "" - - property string message: "" - - property var transactionParams - - signal openStickerPackPopup(string stickerPackId) - signal addEmoji(bool isProfileClick, bool isSticker, bool isImage , var image, bool isEmoji, bool hideEmojiPicker) - signal clickMessage(bool isProfileClick, bool isSticker, bool isImage, var image, bool isEmoji, bool hideEmojiPicker, bool isReply, bool isRightClickOnImage, string imageSource) - signal replyClicked(string messageId, string author) - signal imageClicked(var image) - - - function setMessageActive(messageId, active) { - if (active) { - activeMessage = messageId; - } else if (activeMessage === messageId) { - activeMessage = ""; - } - } - - - width: parent.width - height: messageContainer.height + messageContainer.anchors.topMargin - + (dateGroupLbl.visible ? dateGroupLbl.height + dateGroupLbl.anchors.topMargin : 0) - - Connections { - target: !!root.messageStore && root.messageStore.messageModule ? - root.messageStore.messageModule : null - enabled: !!root.messageStore && !!root.messageStore.messageModule && responseTo !== "" - onRefreshAMessageUserRespondedTo: { - if(msgId === messageId) - chatReply.resetOriginalMessage() - } - } - - Timer { - id: ensureMessageFullyVisibleTimer - interval: 1 - onTriggered: { - chatLogView.positionViewAtIndex(ListView.currentIndex, ListView.Contain) - } - } - - MessageMouseArea { - enabled: !root.isChatBlocked && !placeholderMessage && !isImage - anchors.fill: messageContainer - acceptedButtons: activityCenterMessage ? Qt.LeftButton : Qt.RightButton - messageContextMenu: root.messageContextMenu - messageContextMenuParent: root - isHovered: root.isHovered - isMessageActive: root.isMessageActive - isActivityCenterMessage: activityCenterMessage - stickersLoaded: root.stickersLoaded - onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, null, isEmoji, false, false, false, ""); - } - } - - ChatButtonsPanel { - contentType: messageContentType - parentIsHovered: !editModeOn && isHovered - isChatBlocked: root.isChatBlocked - onHoverChanged: { - hovered && setHovered(messageId, hovered) - } - anchors.right: parent.right - anchors.rightMargin: 20 - anchors.top: messageContainer.top - // This is not exactly like the design because the hover becomes messed up with the buttons on top of another Message - anchors.topMargin: -Style.current.halfPadding - messageContextMenu: root.messageContextMenu - isInPinnedPopup: root.isInPinnedPopup - fromAuthor: senderId - editBtnActive: isText && !editModeOn && root.amISender - pinButtonActive: { - if (!root.messageStore) - return false - - const chatType = root.messageStore.getChatType(); - const amIChatAdmin = root.messageStore.amIChatAdmin(); - const pinMessageAllowedForMembers = root.messageStore.pinMessageAllowedForMembers() - - return chatType === Constants.chatType.oneToOne || - chatType === Constants.chatType.privateGroupChat && amIChatAdmin || - chatType === Constants.chatType.communityChat && (amIChatAdmin || pinMessageAllowedForMembers); - - } - deleteButtonActive: { - if (!root.messageStore) - return false; - const isMyMessage = senderId !== "" && senderId === userProfile.pubKey; - const chatType = root.messageStore.getChatType(); - return isMyMessage && - (contentType === Constants.messageContentType.messageType || - contentType === Constants.messageContentType.stickerType || - contentType === Constants.messageContentType.emojiType || - contentType === Constants.messageContentType.imageType || - contentType === Constants.messageContentType.audioType); - } - pinnedMessage: root.pinnedMessage - canPin: root.canPin - - activityCenterMsg: activityCenterMessage - placeholderMsg: placeholderMessage - onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, image, isEmoji, hideEmojiPicker, false, false, ""); - } - onReplyClicked: { - root.replyClicked(messageId, author) - } - } - - Loader { - active: typeof root.messageContextMenu !== "undefined" - sourceComponent: Component { - Connections { - enabled: isMessageActive - target: root.messageContextMenu - onClosed: root.setMessageActive(messageId, false) - } - } - } - - DateGroup { - id: dateGroupLbl - previousMessageIndex: prevMessageIndex - previousMessageTimestamp: prevMsgTimestamp - messageTimestamp: timestamp - isActivityCenterMessage: activityCenterMessage - } - - function startMessageFoundAnimation() { - messageFoundAnimation.start(); - } - - SequentialAnimation { - id: messageFoundAnimation - PauseAnimation { - duration: 600 - } - NumberAnimation { - target: highlightRect - property: "opacity" - to: 1.0 - duration: 1500 - } - PauseAnimation { - duration: 1000 - } - NumberAnimation { - target: highlightRect - property: "opacity" - to: 0.0 - duration: 1500 - } - } - - Rectangle { - id: highlightRect - anchors.fill: messageContainer - opacity: 0 - visible: (opacity > 0.001) - color: Style.current.backgroundHoverLight - } - - Rectangle { - id: messageContainer - - property alias messageContent: messageContent - - anchors.top: dateGroupLbl.visible ? dateGroupLbl.bottom : parent.top - anchors.topMargin: dateGroupLbl.visible ? (activityCenterMessage ? 4 : Style.current.padding) : 0 - height: childrenRect.height - + (chatName.visible || emojiReactionLoader.active ? Style.current.halfPadding : 0) - + (chatName.visible && emojiReactionLoader.active ? Style.current.padding : 0) - + (!chatName.visible && chatImageContent.active ? 6 : 0) - + (emojiReactionLoader.active ? emojiReactionLoader.height: 0) - + (retry.visible && !chatTime.visible ? Style.current.smallPadding : 0) - + (pinnedRectangleLoader.active ? Style.current.smallPadding : 0) - + (editModeOn ? 25 : 0) - + (!chatName.visible ? 6 : 0) - width: parent.width - - color: { - if (editModeOn) { - return Style.current.backgroundHoverLight - } - - if (activityCenterMessage) { - return read ? Style.current.transparent : Utils.setColorAlpha(Style.current.blue, 0.1) - } - - if (placeholderMessage) { - return Style.current.transparent - } - - if (pinnedMessage) { - return isHovered || isMessageActive ? Style.current.pinnedMessageBackgroundHovered : Style.current.pinnedMessageBackground - } - - return isHovered || isMessageActive ? (hasMention ? Style.current.mentionMessageHoverColor : Style.current.backgroundHoverLight) : - (hasMention ? Style.current.mentionMessageColor : Style.current.transparent) - } - - Loader { - id: pinnedRectangleLoader - active: !editModeOn && pinnedMessage - anchors.left: chatName.left - anchors.top: parent.top - anchors.topMargin: active ? Style.current.halfPadding : 0 - - sourceComponent: Component { - Rectangle { - id: pinnedRectangle - height: 24 - width: childrenRect.width + Style.current.smallPadding - color: Style.current.pinnedRectangleBackground - radius: 12 - - SVGImage { - id: pinImage - source: Style.svg("pin") - anchors.left: parent.left - anchors.leftMargin: 3 - width: 16 - height: 16 - anchors.verticalCenter: parent.verticalCenter - - ColorOverlay { - anchors.fill: parent - source: parent - color: Style.current.pinnedMessageBorder - } - } - - StyledText { - text: qsTr("Pinned by %1").arg(Utils.getContactDetailsAsJson(messagePinnedBy).displayName) - anchors.left: pinImage.right - anchors.verticalCenter: parent.verticalCenter - font.pixelSize: 13 - } - } - } - } - - - // Not Refactored Yet -// Connections { -// enabled: !!rootStore -// target: enabled ? rootStore.chatsModelInst.messageView : null -// onMessageEdited: { -// if(chatReply.item) -// chatReply.item.messageEdited(editedMessageId, editedMessageContent) -// } -// } - - ChatReplyPanel { - id: chatReply - anchors.top: pinnedRectangleLoader.active ? pinnedRectangleLoader.bottom : parent.top - anchors.topMargin: active ? 4 : 0 - anchors.left: chatImage.left - anchors.right: parent.right - anchors.rightMargin: Style.current.padding - isCurrentUser: root.amISender - longReply: active && textFieldImplicitWidth > width - container: root.container - chatHorizontalPadding: chatHorizontalPadding - active: responseTo !== "" && !activityCenterMessage - - function resetOriginalMessage() { - if(!root.messageStore) - return - let obj = root.messageStore.getMessageByIdAsJson(responseTo) - if(!obj) - return - - amISenderOfTheRepliedMessage = obj.amISender - repliedMessageContentType = obj.contentType - repliedMessageSenderIcon = obj.senderIcon - // TODO: not sure about is edited at the moment - repliedMessageIsEdited = false - repliedMessageSender = obj.senderDisplayName - repliedMessageSenderPubkey = obj.senderId - repliedMessageSenderIsAdded = obj.senderIsAdded - repliedMessageContent = obj.messageText - repliedMessageImage = obj.messageImage - stickerData = obj.sticker - } - - Component.onCompleted: { - resetOriginalMessage() - } - - onScrollToBottom: { - // Not Refactored Yet -// messageStore.scrollToBottom(isit, root.container); - } - - onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, image, isEmoji, hideEmojiPicker, isReply, false, "") - } - } - - - UserImage { - id: chatImage - - active: isMessage && headerRepeatCondition - - anchors.left: parent.left - anchors.leftMargin: Style.current.padding - anchors.top: chatReply.active ? chatReply.bottom : - pinnedRectangleLoader.active ? pinnedRectangleLoader.bottom : parent.top - anchors.topMargin: chatReply.active || pinnedRectangleLoader.active ? 4 : Style.current.smallPadding - - image: root.senderIcon - pubkey: senderId - name: senderDisplayName - messageContextMenu: root.messageContextMenu - - onClicked: root.clickMessage(true, false, false, null, false, false, false, false, "") - } - - UsernameLabel { - id: chatName - visible: !editModeOn && isMessage && headerRepeatCondition - anchors.leftMargin: chatHorizontalPadding - anchors.top: chatImage.top - anchors.left: chatImage.right - messageContextMenu: root.messageContextMenu - displayName: senderDisplayName - localName: senderLocalName - amISender: root.amISender - onClickMessage: { - root.clickMessage(true, false, false, null, false, false, false, false, "") - } - } - - VerificationLabel { - id: trustStatus - anchors.left: chatName.right - anchors.leftMargin: 4 - anchors.bottom: chatName.bottom - anchors.bottomMargin: 4 - visible: !root.amISender && chatName.visible - trustStatus: senderTrustStatus - } - - ChatTimePanel { - id: chatTime - visible: !editModeOn && headerRepeatCondition - anchors.verticalCenter: chatName.verticalCenter - anchors.left: trustStatus.right - anchors.leftMargin: 4 - color: Style.current.secondaryText - timestamp: messageTimestamp - } - - Loader { - id: editMessageLoader - active: editModeOn - anchors.top: chatReply.active ? chatReply.bottom : parent.top - anchors.left: chatImage.right - anchors.leftMargin: chatHorizontalPadding - anchors.right: parent.right - anchors.rightMargin: chatHorizontalPadding - height: (item !== null && typeof(item)!== 'undefined')? item.height: 0 - sourceComponent: Item { - id: editText - height: childrenRect.height - - property bool suggestionsOpened: false - Keys.onEscapePressed: { - if (!suggestionsOpened) { - cancelBtn.clicked() - } - suggestionsOpened = false - } - - StatusChatInput { - id: editTextInput - - store: root.store - usersStore: root.usersStore - - chatInputPlaceholder: qsTr("Pinned by %1") - chatType: messageStore.getChatType() - isEdit: true - emojiPopup: root.emojiPopup - messageContextMenu: root.messageContextMenu - onSendMessage: { - saveBtn.clicked(null) - } - suggestions.onVisibleChanged: { - if (suggestions.visible) { - editText.suggestionsOpened = true - } - } - - Component.onCompleted: { - let mentionsMap = new Map() - let index = 0 - while (true) { - index = message.indexOf("", index) + 4 - if (endIndex < 0) { - index += 8 // " ' - mentionsMap.set(mentionLink, mentionTag) - index += linkTag.length - } - - var text = message - for (let [key, value] of mentionsMap) { - text = text.replace(new RegExp(key, 'g'), value) - } - editTextInput.textInput.text = text - editTextInput.textInput.cursorPosition = editTextInput.textInput.length - } - } - - StatusQControls.StatusFlatButton { - id: cancelBtn - anchors.left: parent.left - anchors.leftMargin: Style.current.halfPadding - anchors.top: editTextInput.bottom - text: qsTr("Cancel") - onClicked: { - messageStore.setEditModeOff(messageId) - editTextInput.textInput.text = StatusQUtils.Emoji.parse(message) - ensureMessageFullyVisibleTimer.start() - } - } - - StatusQControls.StatusButton { - id: saveBtn - anchors.left: cancelBtn.right - anchors.leftMargin: Style.current.halfPadding - anchors.top: editTextInput.bottom - text: qsTr("Save") - enabled: editTextInput.textInput.text.trim().length > 0 - onClicked: { - let msg = rootStore.plainText(StatusQUtils.Emoji.deparse(editTextInput.textInput.text)) - if (msg.length > 0){ - msg = messageStore.interpretMessage(msg) - messageStore.setEditModeOff(messageId) - messageStore.editMessage(messageId, msg) - } - } - } - } - } - - Item { - id: messageContent - height: childrenRect.height + (isEmoji ? 2 : 0) - anchors.top: chatName.visible ? chatName.bottom : - chatReply.active ? chatReply.bottom : - pinnedRectangleLoader.active ? pinnedRectangleLoader.bottom : parent.top - anchors.left: parent.left - anchors.leftMargin: chatImage.imageWidth + Style.current.padding + root.chatHorizontalPadding - anchors.right: parent.right - anchors.rightMargin: root.chatHorizontalPadding - anchors.topMargin: (!chatName.visible || !chatReply.active || !pinnedRectangleLoader.active) ? 4 : 0 - visible: !editModeOn - ChatTextView { - id: chatText - readonly property int leftPadding: chatImage.anchors.leftMargin + chatImage.width + chatHorizontalPadding - visible: isText || isEmoji || (isImage && root.message !== "

Update to latest version to see a nice image here!

") - - message: Utils.removeGifUrls(root.message) - anchors.top: parent.top - anchors.topMargin: isEmoji ? 2 : 0 - anchors.left: parent.left - anchors.right: parent.right - // using a padding instead of a margin let's us select text more easily - anchors.leftMargin: -leftPadding - textField.leftPadding: leftPadding - textField.rightPadding: Style.current.bigPadding - - onLinkActivated: { - if (activityCenterMessage) { - root.clickMessage(false, isSticker, false, null, false, false, false, false, "") - } - } - } - - Loader { - id: chatImageContent - active: isImage - anchors.top: chatText.visible ? chatText.bottom : parent.top - anchors.topMargin: active ? 6 : 0 - z: 51 - sourceComponent: Component { - StatusChatImage { - playing: root.messageStore.playAnimation - imageSource: messageImage - imageWidth: 200 - isActiveChannel: root.isActiveChannel - onClicked: { - if (mouse.button === Qt.LeftButton) { - root.imageClicked(image) - } - else if (mouse.button === Qt.RightButton) { - // Set parent, X & Y positions for the messageContextMenu - root.messageContextMenu.parent = root - root.messageContextMenu.setXPosition = function() { return (mouse.x)} - root.messageContextMenu.setYPosition = function() { return (mouse.y)} - root.clickMessage(false, false, true, image, false, true, false, true, imageSource) - } - } - container: root.container - } - } - } - - Loader { - id: stickerLoader - active: contentType === Constants.messageContentType.stickerType - anchors.top: parent.top - anchors.topMargin: active ? Style.current.halfPadding : 0 - sourceComponent: Component { - Rectangle { - id: stickerContainer - color: Style.current.transparent - border.color: isHovered ? Qt.darker(Style.current.border, 1.1) : Style.current.border - border.width: 1 - radius: 16 - width: stickerId.width + 2 * chatVerticalPadding - height: stickerId.height + 2 * chatVerticalPadding - - StatusSticker { - id: stickerId - anchors.top: parent.top - anchors.topMargin: chatVerticalPadding - anchors.left: parent.left - anchors.leftMargin: chatVerticalPadding - contentType: root.contentType - stickerData: root.sticker - onLoaded: { - if(!root.messageStore) - return - // Not refactored yet - // root.messageStore.scrollToBottom(true, root.container) - } - } - } - } - } - - MessageMouseArea { - id: messageMouseArea - anchors.fill: stickerLoader.active ? stickerLoader : chatText - z: activityCenterMessage ? chatText.z + 1 : chatText.z -1 - enabled: !root.isChatBlocked && !placeholderMessage - messageContextMenu: root.messageContextMenu - messageContextMenuParent: root - isHovered: root.isHovered - isMessageActive: root.isMessageActive - isActivityCenterMessage: activityCenterMessage - stickersLoaded: root.stickersLoaded - onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, null, false, false, false, false, ""); - } - onOpenStickerPackPopup: { - root.openStickerPackPopup(root.stickerPack); - } - - onSetMessageActive: { - root.setMessageActive(messageId, active); - } - } - - Loader { - id: linksLoader - active: !!linkUrls - height: item ? item.height : 0 - anchors.top: chatText.bottom - anchors.topMargin: active ? Style.current.halfPadding : 0 - - sourceComponent: Component { - LinksMessageView { - linkUrls: root.linkUrls - container: root.container - messageStore: root.messageStore - store: root.store - isCurrentUser: root.amISender - } - } - } - - Loader { - id: audioPlayerLoader - active: isAudio - anchors.top: parent.top - anchors.topMargin: active ? Style.current.halfPadding : 0 - - sourceComponent: Component { - AudioPlayerPanel { - audioSource: audio - } - } - } - - Loader { - id: transactionBubbleLoader - active: contentType === Constants.messageContentType.transactionType - anchors.top: parent.top - anchors.topMargin: active ? (chatName.visible ? 4 : 6) : 0 - sourceComponent: Component { - TransactionBubbleView { - transactionParams: root.transactionParams - store: root.store - contactsStore: root.contactsStore - } - } - } - - Loader { - active: contentType === Constants.messageContentType.communityInviteType - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: active ? 8 : 0 - sourceComponent: Component { - id: invitationBubble - InvitationBubbleView { - store: root.store - communityId: root.communityId - } - } - } - } - - - Retry { - id: retry - height: visible ? implicitHeight : 0 - anchors.left: chatTime.visible ? chatTime.right : messageContent.left - anchors.leftMargin: chatTime.visible ? chatHorizontalPadding : 0 - anchors.top: chatTime.visible ? chatTime.top : messageContent.bottom - anchors.topMargin: chatTime.visible ? 0 : -4 - anchors.bottom: chatTime.visible ? chatTime.bottom : undefined - isCurrentUser: root.amISender - isExpired: isExpired - timeout: timeout - onClicked: { - // Not Refactored Yet -// rootStore.chatsModelInst.messageView.resendMessage(chatId, messageId) - } - } - } - - Loader { - active: !activityCenterMessage && (hasMention || pinnedMessage) - height: messageContainer.height - anchors.left: messageContainer.left - anchors.top: messageContainer.top - - sourceComponent: Component { - Rectangle { - id: mentionBorder - color: pinnedMessage ? Style.current.pinnedMessageBorder : Style.current.mentionColor - width: 2 - height: parent.height - } - } - } - - HoverHandler { - enabled: !activityCenterMessage && !chatLogView.flickingVertically && - (forceHoverHandler || (typeof root.messageContextMenu !== "undefined" && typeof Global.profilePopupOpened !== "undefined" && - !root.messageContextMenu.opened && !Global.profilePopupOpened && !Global.popupOpened)) - onHoveredChanged: { - setHovered(messageId, hovered); - } - } - - Loader { - id: emojiReactionLoader - active: reactionsModel.count > 0 - anchors.bottom: messageContainer.bottom - anchors.bottomMargin: Style.current.halfPadding - anchors.left: messageContainer.left - anchors.leftMargin: messageContainer.messageContent.anchors.leftMargin - - sourceComponent: Component { - EmojiReactionsPanel { - id: emojiRect - store: root.messageStore - emojiReactionsModel: reactionsModel - onHoverChanged: { - setHovered(messageId, hovered) - } - isMessageActive: isMessageActive - isCurrentUser: root.amISender - onAddEmojiClicked: { - if(root.isChatBlocked) - return - - // First set parent, X & Y positions for the messageContextMenu - root.messageContextMenu.parent = emojiRect - root.messageContextMenu.setXPosition = function() { return (root.messageContextMenu.parent.x + root.messageContextMenu.parent.width + 4) } - root.messageContextMenu.setYPosition = function() { return (-root.messageContextMenu.height - 4) } - - // Second, add emoji that also triggers setXYPosition methods / open popup: - root.addEmoji(false, false, false, null, true, false); - } - - onToggleReaction: { - if(root.isChatBlocked) - return - - if(!root.messageStore) - { - console.error("reaction cannot be toggled, message store is not valid") - return - } - - root.messageStore.toggleReaction(messageId, emojiID) - } - - onSetMessageActive: { - root.setMessageActive(messageId, active); - } - } - } - } -} diff --git a/ui/imports/shared/views/chat/MessageContextMenuView.qml b/ui/imports/shared/views/chat/MessageContextMenuView.qml index 53c914763d0..e525e720fdd 100644 --- a/ui/imports/shared/views/chat/MessageContextMenuView.qml +++ b/ui/imports/shared/views/chat/MessageContextMenuView.qml @@ -54,7 +54,7 @@ StatusPopupMenu { readonly property bool isMyMutualContact: { return root.selectedUserPublicKey !== "" && root.store.contactsStore.isMyMutualContact(root.selectedUserPublicKey); } - readonly property bool isBlockedContact: d.contactDetails && d.contactDetails.isBlocked + readonly property bool isBlockedContact: d.contactDetails && !!d.contactDetails.isBlocked readonly property bool hasPendingContactRequest: { return root.selectedUserPublicKey !== "" && root.store.contactsStore.hasPendingContactRequest(root.selectedUserPublicKey); } @@ -62,9 +62,6 @@ StatusPopupMenu { readonly property bool userTrustIsUnknown: d.contactDetails && d.contactDetails.trustStatus === Constants.trustStatus.unknown readonly property bool userIsUntrustworthy: d.contactDetails && d.contactDetails.trustStatus === Constants.trustStatus.untrustworthy - property var setXPosition: function() {return 0} - property var setYPosition: function() {return 0} - property var emojiReactionsReactedByUser: [] signal openProfileClicked(string publicKey, string state) @@ -102,14 +99,6 @@ StatusPopupMenu { d.contactDetails = {} } - onHeightChanged: { root.y = setYPosition(); } - onWidthChanged: { root.x = setXPosition(); } - onOpened: { - // Trigger x and y position: - x = setXPosition() - y = setYPosition() - } - width: Math.max(emojiContainer.visible ? emojiContainer.width : 0, 230) onAboutToShow: { @@ -162,7 +151,8 @@ StatusPopupMenu { displayName: root.selectedUserDisplayName pubkey: root.selectedUserPublicKey icon: root.selectedUserIcon - trustStatus: d.contactDetails.trustStatus + trustStatus: d.contactDetails && d.contactDetails.trustStatus ? d.contactDetails.trustStatus + : Constants.trustStatus.unknown isContact: root.isMyMutualContact isCurrentUser: root.isMe } diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index b93788377fa..ce9067e7990 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -1,38 +1,24 @@ import QtQuick 2.13 -import StatusQ.Components 0.1 - import utils 1.0 import shared.panels 1.0 import shared.status 1.0 import shared.controls 1.0 +import shared.popups 1.0 import shared.panels.chat 1.0 import shared.views.chat 1.0 import shared.controls.chat 1.0 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + Loader { id: root - width: parent.width - z: (typeof chatLogView === "undefined") ? 1 : (chatLogView.count - index) - - sourceComponent: { - switch(contentType) { - case Constants.messageContentType.chatIdentifier: - return channelIdentifierComponent - case Constants.messageContentType.fetchMoreMessagesButton: - return fetchMoreMessagesButtonComponent - case Constants.messageContentType.systemMessagePrivateGroupType: - return privateGroupHeaderComponent - case Constants.messageContentType.gapType: - return gapComponent - default: - return compactMessageComponent - - } - } - - property var store + property var rootStore property var messageStore property var usersStore property var contactsStore @@ -54,19 +40,20 @@ Loader { property string senderId: "" property string senderDisplayName: "" property string senderLocalName: "" + property string senderEnsName: "" property string senderIcon: "" property bool amISender: false property bool senderIsAdded: false property int senderTrustStatus: Constants.trustStatus.unknown readonly property string senderIconToShow: { if ((!senderIsAdded && - Global.privacyModuleInst.profilePicturesVisibility !== - Constants.profilePicturesVisibility.everyone)) { + Global.privacyModuleInst.profilePicturesVisibility !== + Constants.profilePicturesVisibility.everyone)) { return "" } return senderIcon } - property string message: "" + property string messageText: "" property string messageImage: "" property string messageTimestamp: "" property string messageOutgoingStatus: "" @@ -75,9 +62,15 @@ Loader { property string messagePinnedBy: "" property var reactionsModel: [] property string linkUrls: "" - property bool isInPinnedPopup: false // The pinned popup limits the number of buttons shown property var transactionParams + // External behavior changers + property bool isInPinnedPopup: false // The pinned popup limits the number of buttons shown + property bool disableHover: false // Used to force the HoverHandler to be active (useful for messages in popups) + property bool placeholderMessage: false + property bool activityCenterMessage: false + property bool activityCenterMessageRead: true + property int gapFrom: 0 property int gapTo: 0 @@ -86,33 +79,19 @@ Loader { property int nextMessageIndex: -1 property var nextMessageAsJsonObj - property string hoveredMessage - property string activeMessage - property bool isHovered: typeof hoveredMessage !== "undefined" && hoveredMessage === messageId - property bool isMessageActive: typeof activeMessage !== "undefined" && activeMessage === messageId - property bool editModeOn: false - function setHovered(messageId, hovered) { - if (hovered) { - hoveredMessage = messageId; - } else if (hoveredMessage === messageId) { - hoveredMessage = ""; - } - } + property string responseTo: responseToMessageWithId // Legacy - property string responseTo: responseToMessageWithId property bool isCurrentUser: amISender - property int contentType: messageContentType - property string timestamp: messageTimestamp property string displayUserName: senderDisplayName property string outgoingStatus: messageOutgoingStatus property string authorCurrentMsg: senderId property string authorPrevMsg: { if(!prevMessageAsJsonObj || - // The system message for private groups appear as created by the group host, but it shouldn't - prevMessageAsJsonObj.contentType === Constants.messageContentType.systemMessagePrivateGroupType) { + // The system message for private groups appear as created by the group host, but it shouldn't + prevMessageAsJsonObj.contentType === Constants.messageContentType.systemMessagePrivateGroupType) { return "" } @@ -131,7 +110,7 @@ Loader { return nextMessageAsJsonObj.timestamp } - property bool shouldRepeatHeader: ((parseInt(timestamp, 10) - parseInt(prevMsgTimestamp, 10)) / 60 / 1000) > Constants.repeatHeaderInterval + property bool shouldRepeatHeader: ((parseInt(messageTimestamp, 10) - parseInt(prevMsgTimestamp, 10)) / 60 / 1000) > Constants.repeatHeaderInterval ////////////////////////////////////// //TODO CHECCK - REMOVE @@ -139,41 +118,40 @@ Loader { property string emojiReactions: "" property bool timeout: false property bool hasMention: false - property bool placeholderMessage: false - property bool activityCenterMessage: false - property bool read: true - property bool forceHoverHandler: false // Used to force the HoverHandler to be active (useful for messages in popups) property string replaces: "" - property bool isEdited: false property bool stickersLoaded: false ////////////////////////////////////// + property bool isEdited: false + property string sticker: "Qme8vJtyrEHxABcSVGPF95PtozDgUyfr1xGjePmFdZgk9v" property int stickerPack: -1 - property bool isEmoji: contentType === Constants.messageContentType.emojiType - property bool isImage: contentType === Constants.messageContentType.imageType - property bool isAudio: contentType === Constants.messageContentType.audioType - property bool isStatusMessage: contentType === Constants.messageContentType.systemMessagePrivateGroupType - property bool isSticker: contentType === Constants.messageContentType.stickerType - property bool isText: contentType === Constants.messageContentType.messageType || contentType === Constants.messageContentType.editType + property bool isEmoji: messageContentType === Constants.messageContentType.emojiType + property bool isImage: messageContentType === Constants.messageContentType.imageType + property bool isAudio: messageContentType === Constants.messageContentType.audioType + property bool isStatusMessage: messageContentType === Constants.messageContentType.systemMessagePrivateGroupType + property bool isSticker: messageContentType === Constants.messageContentType.stickerType + property bool isText: messageContentType === Constants.messageContentType.messageType || messageContentType === Constants.messageContentType.editType property bool isMessage: isEmoji || isImage || isSticker || isText || isAudio - || contentType === Constants.messageContentType.communityInviteType || contentType === Constants.messageContentType.transactionType + || messageContentType === Constants.messageContentType.communityInviteType || messageContentType === Constants.messageContentType.transactionType - property bool isExpired: (outgoingStatus === "sending" && (Math.floor(timestamp) + 180000) < Date.now()) + property bool isExpired: (outgoingStatus === "sending" && (Math.floor(messageTimestamp) + 180000) < Date.now()) property int statusAgeEpoch: 0 signal imageClicked(var image) property var scrollToBottom: function () {} - property var clickMessage: function(isProfileClick, - isSticker = false, - isImage = false, - image = null, - isEmoji = false, - hideEmojiPicker = false, - isReply = false, - isRightClickOnImage = false, - imageSource = "") { + // WARNING: To much arguments here. Create an object argument. + property var messageClickHandler: function(sender, point, + isProfileClick, + isSticker = false, + isImage = false, + image = null, + isEmoji = false, + hideEmojiPicker = false, + isReply = false, + isRightClickOnImage = false, + imageSource = "") { if (placeholderMessage || activityCenterMessage) { return @@ -188,7 +166,7 @@ Loader { messageContextMenu.messageSenderId = root.senderId messageContextMenu.messageContentType = root.messageContentType messageContextMenu.pinnedMessage = root.pinnedMessage - messageContextMenu.canPin = messageStore.getNumberOfPinnedMessages() < Constants.maxNumberOfPins + messageContextMenu.canPin = d.canPin messageContextMenu.selectedUserPublicKey = root.senderId messageContextMenu.selectedUserDisplayName = root.senderDisplayName @@ -202,7 +180,7 @@ Loader { messageContextMenu.isSticker = isSticker messageContextMenu.hideEmojiPicker = hideEmojiPicker - if(isReply){ + if (isReply){ let obj = messageStore.getMessageByIdAsJson(responseTo) if(!obj) return @@ -213,15 +191,16 @@ Loader { messageContextMenu.selectedUserIcon = obj.senderIcon } - messageContextMenu.popup() + messageContextMenu.parent = sender; + messageContextMenu.popup(point); } signal showReplyArea(string messageId, string author) -// function showReactionAuthors(fromAccounts, emojiId) { -// return root.rootStore.showReactionAuthors(fromAccounts, emojiId) -// } + // function showReactionAuthors(fromAccounts, emojiId) { + // return root.rootStore.showReactionAuthors(fromAccounts, emojiId) + // } function startMessageFoundAnimation() { root.item.startMessageFoundAnimation(); @@ -231,23 +210,76 @@ Loader { signal openStickerPackPopup(string stickerPackId) // Not Refactored Yet -// Connections { -// enabled: (!placeholderMessage && !!root.rootStore) -// target: !!root.rootStore ? root.rootStore.allContacts : null -// onContactChanged: { -// if (pubkey === fromAuthor) { -// const img = appMain.getProfileImage(userPubKey, isCurrentUser, useLargeImage) -// if (img) { -// profileImageSource = img -// } -// } else if (replyMessageIndex > -1 && pubkey === repliedMessageAuthorPubkey) { -// const imgReply = appMain.getProfileImage(repliedMessageAuthorPubkey, repliedMessageAuthorIsCurrentUser, false) -// if (imgReply) { -// repliedMessageUserImage = imgReply -// } -// } -// } -// } + // Connections { + // enabled: (!placeholderMessage && !!root.rootStore) + // target: !!root.rootStore ? root.rootStore.allContacts : null + // onContactChanged: { + // if (pubkey === fromAuthor) { + // const img = appMain.getProfileImage(userPubKey, isCurrentUser, useLargeImage) + // if (img) { + // profileImageSource = img + // } + // } else if (replyMessageIndex > -1 && pubkey === repliedMessageAuthorPubkey) { + // const imgReply = appMain.getProfileImage(repliedMessageAuthorPubkey, repliedMessageAuthorIsCurrentUser, false) + // if (imgReply) { + // repliedMessageUserImage = imgReply + // } + // } + // } + // } + + z: (typeof chatLogView === "undefined") ? 1 : (chatLogView.count - index) + + sourceComponent: { + switch(messageContentType) { + case Constants.messageContentType.chatIdentifier: + return channelIdentifierComponent + case Constants.messageContentType.fetchMoreMessagesButton: + return fetchMoreMessagesButtonComponent + case Constants.messageContentType.systemMessagePrivateGroupType: + return privateGroupHeaderComponent + case Constants.messageContentType.gapType: + return gapComponent + default: + return messageComponent + } + } + + QtObject { + id: d + + readonly property bool canPin: !!messageStore && + messageStore.getNumberOfPinnedMessages() < Constants.maxNumberOfPins + readonly property int chatButtonSize: 32 + + property string activeMessage + readonly property bool isMessageActive: typeof activeMessage !== "undefined" && activeMessage === messageId + + function setMessageActive(messageId, active) { + + // TODO: Is argument messageId actually needed? + // It was probably used with dynamic scoping, + // but not this method can be moved to private `d`. + // Probably that it was done this way, because `MessageView` is reused as delegate. + + if (active) { + d.activeMessage = messageId; + return; + } + if (d.activeMessage === messageId) { + d.activeMessage = ""; + return; + } + } + } + + Connections { + enabled: d.isMessageActive + target: root.messageContextMenu + onClosed: { + d.setMessageActive(root.messageId, false) + } + } Component { id: gapComponent @@ -293,18 +325,18 @@ Loader { wrapMode: Text.Wrap text: { return ``+ - ``+ - ``+ - ``+ - ``+ - `${message}`+ - ``+ - ``; + `}`+ + ``+ + ``+ + ``+ + `${messageText}`+ + ``+ + ``; } visible: isStatusMessage font.pixelSize: 14 @@ -318,56 +350,458 @@ Loader { } Component { - id: compactMessageComponent - - CompactMessageView { - container: root - store: root.store - message: root.message - messageStore: root.messageStore - usersStore: root.usersStore - contactsStore: root.contactsStore - messageContextMenu: root.messageContextMenu - contentType: root.messageContentType - isChatBlocked: root.isChatBlocked - isActiveChannel: root.isActiveChannel - emojiPopup: root.emojiPopup - senderTrustStatus: root.senderTrustStatus - chatLogView: root.chatLogView - - communityId: root.communityId - stickersLoaded: root.stickersLoaded - sticker: root.sticker - stickerPack: root.stickerPack - isMessageActive: root.isMessageActive - senderIcon: root.senderIconToShow - amISender: root.amISender - isHovered: root.isHovered - editModeOn: root.editModeOn - linkUrls: root.linkUrls - isInPinnedPopup: root.isInPinnedPopup - pinnedMessage: root.pinnedMessage - canPin: !!messageStore && messageStore.getNumberOfPinnedMessages() < Constants.maxNumberOfPins - - transactionParams: root.transactionParams - - onAddEmoji: { - root.clickMessage(isProfileClick, isSticker, isImage , image, isEmoji, hideEmojiPicker) + id: messageComponent + + StatusMessage { + id: delegate + + function convertContentType(value) { + switch (value) { + case Constants.messageContentType.messageType: + return StatusMessage.ContentType.Text; + case Constants.messageContentType.stickerType: + return StatusMessage.ContentType.Sticker; + case Constants.messageContentType.emojiType: + return StatusMessage.ContentType.Emoji; + case Constants.messageContentType.transactionType: + return StatusMessage.ContentType.Transaction; + case Constants.messageContentType.imageType: + return StatusMessage.ContentType.Image; + case Constants.messageContentType.audioType: + return StatusMessage.ContentType.Audio; + case Constants.messageContentType.communityInviteType: + return StatusMessage.ContentType.Invitation; + case Constants.messageContentType.fetchMoreMessagesButton: + case Constants.messageContentType.chatIdentifier: + case Constants.messageContentType.unknownContentType: + case Constants.messageContentType.statusType: + case Constants.messageContentType.systemMessagePrivateGroupType: + case Constants.messageContentType.gapType: + case Constants.messageContentType.editType: + default: + return StatusMessage.ContentType.Unknown; + } + } + + readonly property int contentType: convertContentType(root.messageContentType) + readonly property bool isReply: root.responseTo !== "" + readonly property var replyMessage: root.messageStore && isReply ? root.messageStore.getMessageByIdAsJson(root.responseTo) : null + readonly property string replySenderId: replyMessage ? replyMessage.senderId : "" + + function editCompletedHandler(newMessageText) { + const message = root.rootStore.plainText(StatusQUtils.Emoji.deparse(newMessageText)) + if (message.length <= 0) + return; + + const interpretedMessage = root.messageStore.interpretMessage(message) + root.messageStore.setEditModeOff(root.messageId) + root.messageStore.editMessage(root.messageId, interpretedMessage) + } + + audioMessageInfoText: qsTr("Audio Message") + cancelButtonText: qsTr("Cancel") + saveButtonText: qsTr("Save") + loadingImageText: qsTr("Loading image...") + errorLoadingImageText: qsTr("Error loading the image") + resendText: qsTr("Resend") + pinnedMsgInfoText: qsTr("Pinned by") + reactionIcons: [ + Style.svg("emojiReactions/heart"), + Style.svg("emojiReactions/thumbsUp"), + Style.svg("emojiReactions/thumbsDown"), + Style.svg("emojiReactions/laughing"), + Style.svg("emojiReactions/sad"), + Style.svg("emojiReactions/angry"), + ] + + timestamp: root.messageTimestamp + isAReply: delegate.isReply + isEdited: root.isEdited + hasMention: root.hasMention + isPinned: root.pinnedMessage + pinnedBy: root.pinnedMessage ? Utils.getContactDetailsAsJson(root.messagePinnedBy).displayName : "" + hasExpired: root.isExpired + reactionsModel: root.reactionsModel + + previousMessageIndex: root.prevMessageIndex + previousMessageTimestamp: root.prevMsgTimestamp + + showHeader: root.authorCurrentMsg !== root.authorPrevMsg || + root.shouldRepeatHeader || dateGroupVisible || isAReply + isActiveMessage: d.isMessageActive + + disableHover: root.disableHover || + (root.chatLogView && root.chatLogView.flickingVertically) || + activityCenterMessage || + root.messageContextMenu.opened || + !!Global.profilePopupOpened || + !!Global.popupOpened + + hideQuickActions: root.isChatBlocked || + root.placeholderMessage || + root.activityCenterMessage + + overrideBackground: root.activityCenterMessage || root.placeholderMessage + overrideBackgroundColor: { + if (root.activityCenterMessage && root.activityCenterMessageRead) + return Utils.setColorAlpha(Style.current.blue, 0.1); + return "transparent"; + } + + editMode: root.editModeOn + + onEditCancelled: { + root.messageStore.setEditModeOff(root.messageId) + } + + onEditCompleted: { + delegate.editCompletedHandler(newMsgText) + } + + onImageClicked: { + switch (mouse.button) { + case Qt.LeftButton: + root.imageClicked(image, mouse); + break; + case Qt.RightButton: + root.messageClickHandler(image, Qt.point(mouse.x, mouse.y), false, false, true, image, false, true, false, true, imageSource) + break; + } + } + + onLinkActivated: { + if (link.startsWith('//')) { + const pubkey = link.replace("//", ""); + Global.openProfilePopup(pubkey) + return; + } + + Global.openLink(link) + } + + onProfilePictureClicked: { + d.setMessageActive(root.messageId, true); + root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true); + } + + onReplyProfileClicked: { + d.setMessageActive(root.messageId, true); + root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true, false, false, null, false, false, true); + } + + onSenderNameClicked: { + d.setMessageActive(root.messageId, true); + root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), true); + } + + onToggleReactionClicked: { + if (root.isChatBlocked) + return + + if (!root.messageStore) { + console.error("Reaction can not be toggled, message store is not valid") + return + } + + root.messageStore.toggleReaction(root.messageId, emojiId) + } + + onAddReactionClicked: { + if (root.isChatBlocked) + return; + + d.setMessageActive(root.messageId, true); + root.messageClickHandler(sender, Qt.point(mouse.x, mouse.y), false, false, false, null, true, false); + } + + onStickerClicked: { + root.openStickerPackPopup(root.stickerPack); + } + + mouseArea { + acceptedButtons: root.activityCenterMessage ? Qt.LeftButton : Qt.RightButton + enabled: !root.isChatBlocked && + !root.placeholderMessage && + delegate.contentType !== StatusMessage.ContentType.Image + onClicked: { + d.setMessageActive(root.messageId, true); + root.messageClickHandler(this, Qt.point(mouse.x, mouse.y), + false, false, false, null, root.isEmoji, false, false, false, ""); + } } - onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, image, isEmoji, hideEmojiPicker, isReply, isRightClickOnImage, imageSource) + messageDetails: StatusMessageDetails { + contentType: delegate.contentType + messageText: root.messageText + messageContent: { + switch (delegate.contentType) + { + case StatusMessage.ContentType.Sticker: + return root.sticker; + case StatusMessage.ContentType.Image: + return root.messageImage; + } + return ""; + } + + amISender: root.amISender + sender.id: root.senderId + sender.userName: root.senderDisplayName + sender.localName: root.senderLocalName + sender.ensName: root.senderEnsName + sender.isContact: root.senderIsAdded + sender.trustIndicator: root.senderTrustStatus + sender.profileImage { + width: 40 + height: 40 + pubkey: root.senderId + source: root.senderIcon || "" + colorId: Utils.colorIdForPubkey(root.senderId) + colorHash: Utils.getColorHashAsJson(root.senderId) + } + } - onOpenStickerPackPopup: { - root.openStickerPackPopup(stickerPackId); + replyDetails: StatusMessageDetails { + messageText: delegate.replyMessage ? delegate.replyMessage.messageText : "" + contentType: delegate.replyMessage ? delegate.convertContentType(delegate.replyMessage.contentType) : 0 + messageContent: { + if (!delegate.replyMessage) + return ""; + + switch (delegate.contentType) { + case StatusMessage.ContentType.Sticker: + return delegate.replyMessage.sticker; + case StatusMessage.ContentType.Image: + return delegate.replyMessage.messageImage; + } + return ""; + } + + amISender: delegate.replyMessage && delegate.replyMessage.amISender + sender.id: delegate.replyMessage ? delegate.replyMessage.senderId : "" + sender.isContact: delegate.replyMessage && delegate.replyMessage.senderIsAdded + sender.userName: delegate.replyMessage ? delegate.replyMessage.senderDisplayName: "" + sender.ensName: delegate.replyMessage && delegate.replyMessage.ensVerified ? delegate.replyMessage.senderDisplayName : "" + sender.localName: delegate.replyMessage ? delegate.replyMessage.senderLocalName : "" + sender.profileImage { + width: 20 + height: 20 + pubkey: delegate.replySenderId + source: delegate.replyMessage ? delegate.replyMessage.senderIcon: "" + colorId: Utils.colorIdForPubkey(delegate.replySenderId) + colorHash: Utils.getColorHashAsJson(delegate.replySenderId) + } } - onReplyClicked: { - root.showReplyArea(messageId, author) + statusChatInput: StatusChatInput { + id: editTextInput + + readonly property string messageText: editTextInput.textInput.text + + // TODO: Move this property and Escape handler to StatusChatInput + property bool suggestionsOpened: false + + width: parent.width + + Keys.onEscapePressed: { + if (!suggestionsOpened) { + delegate.editCancelled() + } + suggestionsOpened = false + } + + store: root.rootStore + usersStore: root.usersStore + emojiPopup: root.emojiPopup + messageContextMenu: root.messageContextMenu + + chatType: root.messageStore.getChatType() + isEdit: true + + onSendMessage: { + delegate.editCompletedHandler(editTextInput.textInput.text) + } + + suggestions.onVisibleChanged: { + if (suggestions.visible) { + suggestionsOpened = true + } + } + + Component.onCompleted: { + parseMessage(root.messageText); + } } - onImageClicked: root.imageClicked(image) + linksComponent: Component { + LinksMessageView { + linkUrls: root.linkUrls + container: root + messageStore: root.messageStore + store: root.rootStore + isCurrentUser: root.amISender + } + } + + transcationComponent: Component { + TransactionBubbleView { + transactionParams: root.transactionParams + store: root.rootStore + contactsStore: root.contactsStore + } + } + + invitationComponent: Component { + InvitationBubbleView { + store: root.rootStore + communityId: root.communityId + } + } + + quickActions: [ + Loader { + active: !root.isInPinnedPopup + sourceComponent: StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: "reaction-b" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Add reaction") + onClicked: { + d.setMessageActive(root.messageId, true) + root.messageClickHandler(this, Qt.point(mouse.x, mouse.y), false, false, false, null, true, false) + } + } + }, + Loader { + active: !root.isInPinnedPopup + sourceComponent: StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: "reply" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Reply") + onClicked: { + root.showReplyArea(root.messageId, root.senderId) + if (messageContextMenu.closeParentPopup) { + messageContextMenu.closeParentPopup() + } + } + } + }, + Loader { + active: !root.isInPinnedPopup && root.isText && !root.editModeOn && root.amISender + sourceComponent: StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: "edit_pencil" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Edit") + onClicked: { + root.messageStore.setEditModeOn(root.messageId) + } + } + }, + Loader { + active: { + if (!root.messageStore) + return false + + const chatType = root.messageStore.getChatType(); + const amIChatAdmin = root.messageStore.amIChatAdmin(); + const pinMessageAllowedForMembers = root.messageStore.pinMessageAllowedForMembers() + + return chatType === Constants.chatType.oneToOne || + chatType === Constants.chatType.privateGroupChat && amIChatAdmin || + chatType === Constants.chatType.communityChat && (amIChatAdmin || pinMessageAllowedForMembers); + + } + sourceComponent: StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: root.pinnedMessage ? "unpin" : "pin" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: root.pinnedMessage ? qsTr("Unpin") : qsTr("Pin") + onClicked: { + if (root.pinnedMessage) { + messageStore.unpinMessage(root.messageId) + return; + } + + if (d.canPin) { + messageStore.pinMessage(root.messageId) + return; + } + + if (!chatContentModule) { + console.warn("error on open pinned messages limit reached from message context menu - chat content module is not set") + return; + } + + Global.openPopup(pinnedMessagesPopupComponent, { + store: root.rootStore, + messageStore: messageStore, + pinnedMessagesModel: chatContentModule.pinnedMessagesModel, + messageToPin: root.messageId + }); + } + } + }, + Loader { + active: { + if (root.isInPinnedPopup) + return false; + if (!root.messageStore) + return false; + const isMyMessage = senderId !== "" && senderId === userProfile.pubKey; + const chatType = root.messageStore.getChatType(); + return isMyMessage && + (messageContentType === Constants.messageContentType.messageType || + messageContentType === Constants.messageContentType.stickerType || + messageContentType === Constants.messageContentType.emojiType || + messageContentType === Constants.messageContentType.imageType || + messageContentType === Constants.messageContentType.audioType); + } + sourceComponent: StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: "delete" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Delete") + onClicked: { + if (!localAccountSensitiveSettings.showDeleteMessageWarning) { + messageStore.deleteMessage(root.messageId) + } + else { + Global.openPopup(deleteMessageConfirmationDialogComponent) + } + } + } + } + ] + } + } + + Component { + id: deleteMessageConfirmationDialogComponent + + ConfirmationDialog { + header.title: qsTrId("Confirm deleting this message") + confirmationText: qsTrId("Are you sure you want to delete this message? Be aware that other clients are not guaranteed to delete the message as well.") + height: 260 + checkbox.visible: true + executeConfirm: function () { + if (checkbox.checked) { + localAccountSensitiveSettings.showDeleteMessageWarning = false + } + + close() + messageStore.deleteMessage(root.messageId) + } + onClosed: { + destroy() + } } } } diff --git a/ui/imports/shared/views/chat/TransactionBubbleView.qml b/ui/imports/shared/views/chat/TransactionBubbleView.qml index 1ad44efc36f..7b6d53a6ebe 100644 --- a/ui/imports/shared/views/chat/TransactionBubbleView.qml +++ b/ui/imports/shared/views/chat/TransactionBubbleView.qml @@ -250,7 +250,7 @@ Item { StyledText { id: timeText color: Style.current.secondaryText - text: Utils.formatShortTime(timestamp, RootStore.accountSensitiveSettings.is24hTimeFormat) + text: Utils.formatShortTime(messageTimestamp, RootStore.accountSensitiveSettings.is24hTimeFormat) anchors.left: bubbleLoader.active ? bubbleLoader.right : undefined anchors.leftMargin: bubbleLoader.active ? 13 : 0 anchors.right: bubbleLoader.active ? undefined : parent.right diff --git a/ui/imports/shared/xss.js b/ui/imports/shared/xss.js deleted file mode 100644 index f502efa36ff..00000000000 --- a/ui/imports/shared/xss.js +++ /dev/null @@ -1,1318 +0,0 @@ -/** - * NOTICE: - * - * Most this code was copied from https://github.com/leizongmin/js-xss and slightly modifed - * to work with JavaScript resource loading in QML. - */ - -var defaultCSSFilter = new FilterCSS(); - -var DEFAULT = { - whiteList: getDefaultWhiteList(), - getDefaultWhiteList, - onTag, - onIgnoreTag, - onTagAttr, - onIgnoreTagAttr, - safeAttrValue, - escapeHtml, - escapeQuote, - unescapeQuote, - escapeHtmlEntities, - escapeDangerHtml5Entities, - clearNonPrintableCharacter, - friendlyAttrValue, - escapeAttrValue, - onIgnoreTagStripAll, - StripTagBody, - stripCommentTag , - stripBlankChar, - cssFilter: defaultCSSFilter, - getDefaultCSSWhiteList, -} - -/** - * shallow copy - * - * @param {Object} obj - * @return {Object} - */ -function shallowCopyObject(obj) { - var ret = {}; - for (var i in obj) { - ret[i] = obj[i]; - } - return ret; -} - - -var _ = { - indexOf: function(arr, item) { - var i, j; - if (Array.prototype.indexOf) { - return arr.indexOf(item); - } - for (i = 0, j = arr.length; i < j; i++) { - if (arr[i] === item) { - return i; - } - } - return -1; - }, - forEach: function(arr, fn, scope) { - var i, j; - if (Array.prototype.forEach) { - return arr.forEach(fn, scope); - } - for (i = 0, j = arr.length; i < j; i++) { - fn.call(scope, arr[i], i, arr); - } - }, - trim: function(str) { - if (String.prototype.trim) { - return str.trim(); - } - return str.replace(/(^\s*)|(\s*$)/g, ""); - }, - spaceIndex: function(str) { - var reg = /\s|\n|\t/; - var match = reg.exec(str); - return match ? match.index : -1; - } -}; - -function filterXSS(html, options) { - var xss = new FilterXSS(options); - return xss.process(html); -} - -function onAttr (name, value, options) { - // do nothing -} - -function onIgnoreAttr (name, value, options) { - // do nothing -} - -/** - * FilterXSS class - * - * @param {Object} options - * whiteList, onTag, onTagAttr, onIgnoreTag, - * onIgnoreTagAttr, safeAttrValue, escapeHtml - * stripIgnoreTagBody, allowCommentTag, stripBlankChar - * css{whiteList, onAttr, onIgnoreAttr} `css=false` means don't use `cssfilter` - */ -function FilterXSS(options) { - options = shallowCopyObject(options || {}); - - if (options.stripIgnoreTag) { - if (options.onIgnoreTag) { - console.error( - 'Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time' - ); - } - options.onIgnoreTag = onIgnoreTagStripAll; - } - - options.whiteList = options.whiteList || getDefaultWhiteList(); - options.onTag = options.onTag || onTag; - options.onTagAttr = options.onTagAttr || onTagAttr; - options.onIgnoreTag = options.onIgnoreTag || onIgnoreTag; - options.onIgnoreTagAttr = options.onIgnoreTagAttr || onIgnoreTagAttr; - options.safeAttrValue = options.safeAttrValue || safeAttrValue; - options.escapeHtml = options.escapeHtml || escapeHtml; - this.options = options; - - if (options.css === false) { - this.cssFilter = false; - } else { - options.css = options.css || {}; - this.cssFilter = new FilterCSS(options.css); - } -} - -/** - * start process and returns result - * - * @param {String} html - * @return {String} - */ -FilterXSS.prototype.process = function(html) { - // compatible with the input - html = html || ""; - html = html.toString(); - if (!html) return ""; - - var me = this; - var options = me.options; - var whiteList = options.whiteList; - var onTag = options.onTag; - var onIgnoreTag = options.onIgnoreTag; - var onTagAttr = options.onTagAttr; - var onIgnoreTagAttr = options.onIgnoreTagAttr; - var safeAttrValue = options.safeAttrValue; - var escapeHtml = options.escapeHtml; - var cssFilter = me.cssFilter; - - // remove invisible characters - if (options.stripBlankChar) { - html = stripBlankChar(html); - } - - // remove html comments - if (!options.allowCommentTag) { - html = stripCommentTag(html); - } - - // if enable stripIgnoreTagBody - var stripIgnoreTagBody = false; - if (options.stripIgnoreTagBody) { - var stripIgnoreTagBody = StripTagBody( - options.stripIgnoreTagBody, - onIgnoreTag - ); - onIgnoreTag = stripIgnoreTagBody.onIgnoreTag; - } - - var retHtml = parseTag( - html, - function(sourcePosition, position, tag, html, isClosing) { - var info = { - sourcePosition: sourcePosition, - position: position, - isClosing: isClosing, - isWhite: whiteList.hasOwnProperty(tag) - }; - - // call `onTag()` - var ret = onTag(tag, html, info); - if (!isNull(ret)) return ret; - - if (info.isWhite) { - if (info.isClosing) { - return ""; - } - - var attrs = getAttrs(html); - var whiteAttrList = whiteList[tag]; - var attrsHtml = parseAttr(attrs.html, function(name, value) { - // call `onTagAttr()` - var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1; - var ret = onTagAttr(tag, name, value, isWhiteAttr); - if (!isNull(ret)) return ret; - - if (isWhiteAttr) { - // call `safeAttrValue()` - value = safeAttrValue(tag, name, value, cssFilter); - if (value) { - return name + '="' + value + '"'; - } else { - return name; - } - } else { - // call `onIgnoreTagAttr()` - var ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr); - if (!isNull(ret)) return ret; - return; - } - }); - - // build new tag html - var html = "<" + tag; - if (attrsHtml) html += " " + attrsHtml; - if (attrs.closing) html += " /"; - html += ">"; - return html; - } else { - // call `onIgnoreTag()` - var ret = onIgnoreTag(tag, html, info); - if (!isNull(ret)) return ret; - return escapeHtml(html); - } - }, - escapeHtml - ); - - // if enable stripIgnoreTagBody - if (stripIgnoreTagBody) { - retHtml = stripIgnoreTagBody.remove(retHtml); - } - - return retHtml; -}; - - -function getDefaultWhiteList() { - return { - a: ["target", "href", "title", "class"], - abbr: ["title"], - address: [], - area: ["shape", "coords", "href", "alt"], - article: [], - aside: [], - audio: ["autoplay", "controls", "loop", "preload", "src"], - b: [], - bdi: ["dir"], - bdo: ["dir"], - big: [], - blockquote: ["cite"], - br: [], - caption: [], - center: [], - cite: [], - code: [], - col: ["align", "valign", "span", "width"], - colgroup: ["align", "valign", "span", "width"], - dd: [], - del: ["datetime"], - details: ["open"], - div: [], - dl: [], - dt: [], - em: [], - font: ["color", "size", "face"], - footer: [], - h1: [], - h2: [], - h3: [], - h4: [], - h5: [], - h6: [], - header: [], - hr: [], - i: [], - img: ["src", "alt", "title", "width", "height"], - ins: ["datetime"], - li: [], - mark: [], - nav: [], - ol: [], - p: [], - pre: [], - s: [], - section: [], - small: [], - span: [], - sub: [], - sup: [], - strong: [], - table: ["width", "height", "border", "bgcolor", "cellspacing", "cellpadding", "class"], - tbody: ["align", "valign"], - td: ["width", "bgcolor", "rowspan", "colspan", "align", "valign", "class"], - tfoot: ["align", "valign"], - th: ["width", "rowspan", "colspan", "align", "valign"], - thead: ["align", "valign"], - tr: ["rowspan", "align", "valign"], - tt: [], - u: [], - ul: [], - video: ["autoplay", "controls", "loop", "preload", "src", "height", "width"] - }; -} - -function getDefaultCSSWhiteList () { - // 白名单值说明: - // true: 允许该属性 - // Function: function (val) { } 返回true表示允许该属性,其他值均表示不允许 - // RegExp: regexp.test(val) 返回true表示允许该属性,其他值均表示不允许 - // 除上面列出的值外均表示不允许 - var whiteList = {}; - - whiteList['align-content'] = false; // default: auto - whiteList['align-items'] = false; // default: auto - whiteList['align-self'] = false; // default: auto - whiteList['alignment-adjust'] = false; // default: auto - whiteList['alignment-baseline'] = false; // default: baseline - whiteList['all'] = false; // default: depending on individual properties - whiteList['anchor-point'] = false; // default: none - whiteList['animation'] = false; // default: depending on individual properties - whiteList['animation-delay'] = false; // default: 0 - whiteList['animation-direction'] = false; // default: normal - whiteList['animation-duration'] = false; // default: 0 - whiteList['animation-fill-mode'] = false; // default: none - whiteList['animation-iteration-count'] = false; // default: 1 - whiteList['animation-name'] = false; // default: none - whiteList['animation-play-state'] = false; // default: running - whiteList['animation-timing-function'] = false; // default: ease - whiteList['azimuth'] = false; // default: center - whiteList['backface-visibility'] = false; // default: visible - whiteList['background'] = true; // default: depending on individual properties - whiteList['background-attachment'] = true; // default: scroll - whiteList['background-clip'] = true; // default: border-box - whiteList['background-color'] = true; // default: transparent - whiteList['background-image'] = true; // default: none - whiteList['background-origin'] = true; // default: padding-box - whiteList['background-position'] = true; // default: 0% 0% - whiteList['background-repeat'] = true; // default: repeat - whiteList['background-size'] = true; // default: auto - whiteList['baseline-shift'] = false; // default: baseline - whiteList['binding'] = false; // default: none - whiteList['bleed'] = false; // default: 6pt - whiteList['bookmark-label'] = false; // default: content() - whiteList['bookmark-level'] = false; // default: none - whiteList['bookmark-state'] = false; // default: open - whiteList['border'] = true; // default: depending on individual properties - whiteList['border-bottom'] = true; // default: depending on individual properties - whiteList['border-bottom-color'] = true; // default: current color - whiteList['border-bottom-left-radius'] = true; // default: 0 - whiteList['border-bottom-right-radius'] = true; // default: 0 - whiteList['border-bottom-style'] = true; // default: none - whiteList['border-bottom-width'] = true; // default: medium - whiteList['border-collapse'] = true; // default: separate - whiteList['border-color'] = true; // default: depending on individual properties - whiteList['border-image'] = true; // default: none - whiteList['border-image-outset'] = true; // default: 0 - whiteList['border-image-repeat'] = true; // default: stretch - whiteList['border-image-slice'] = true; // default: 100% - whiteList['border-image-source'] = true; // default: none - whiteList['border-image-width'] = true; // default: 1 - whiteList['border-left'] = true; // default: depending on individual properties - whiteList['border-left-color'] = true; // default: current color - whiteList['border-left-style'] = true; // default: none - whiteList['border-left-width'] = true; // default: medium - whiteList['border-radius'] = true; // default: 0 - whiteList['border-right'] = true; // default: depending on individual properties - whiteList['border-right-color'] = true; // default: current color - whiteList['border-right-style'] = true; // default: none - whiteList['border-right-width'] = true; // default: medium - whiteList['border-spacing'] = true; // default: 0 - whiteList['border-style'] = true; // default: depending on individual properties - whiteList['border-top'] = true; // default: depending on individual properties - whiteList['border-top-color'] = true; // default: current color - whiteList['border-top-left-radius'] = true; // default: 0 - whiteList['border-top-right-radius'] = true; // default: 0 - whiteList['border-top-style'] = true; // default: none - whiteList['border-top-width'] = true; // default: medium - whiteList['border-width'] = true; // default: depending on individual properties - whiteList['bottom'] = false; // default: auto - whiteList['box-decoration-break'] = true; // default: slice - whiteList['box-shadow'] = true; // default: none - whiteList['box-sizing'] = true; // default: content-box - whiteList['box-snap'] = true; // default: none - whiteList['box-suppress'] = true; // default: show - whiteList['break-after'] = true; // default: auto - whiteList['break-before'] = true; // default: auto - whiteList['break-inside'] = true; // default: auto - whiteList['caption-side'] = false; // default: top - whiteList['chains'] = false; // default: none - whiteList['clear'] = true; // default: none - whiteList['clip'] = false; // default: auto - whiteList['clip-path'] = false; // default: none - whiteList['clip-rule'] = false; // default: nonzero - whiteList['color'] = true; // default: implementation dependent - whiteList['color-interpolation-filters'] = true; // default: auto - whiteList['column-count'] = false; // default: auto - whiteList['column-fill'] = false; // default: balance - whiteList['column-gap'] = false; // default: normal - whiteList['column-rule'] = false; // default: depending on individual properties - whiteList['column-rule-color'] = false; // default: current color - whiteList['column-rule-style'] = false; // default: medium - whiteList['column-rule-width'] = false; // default: medium - whiteList['column-span'] = false; // default: none - whiteList['column-width'] = false; // default: auto - whiteList['columns'] = false; // default: depending on individual properties - whiteList['contain'] = false; // default: none - whiteList['content'] = false; // default: normal - whiteList['counter-increment'] = false; // default: none - whiteList['counter-reset'] = false; // default: none - whiteList['counter-set'] = false; // default: none - whiteList['crop'] = false; // default: auto - whiteList['cue'] = false; // default: depending on individual properties - whiteList['cue-after'] = false; // default: none - whiteList['cue-before'] = false; // default: none - whiteList['cursor'] = false; // default: auto - whiteList['direction'] = false; // default: ltr - whiteList['display'] = true; // default: depending on individual properties - whiteList['display-inside'] = true; // default: auto - whiteList['display-list'] = true; // default: none - whiteList['display-outside'] = true; // default: inline-level - whiteList['dominant-baseline'] = false; // default: auto - whiteList['elevation'] = false; // default: level - whiteList['empty-cells'] = false; // default: show - whiteList['filter'] = false; // default: none - whiteList['flex'] = false; // default: depending on individual properties - whiteList['flex-basis'] = false; // default: auto - whiteList['flex-direction'] = false; // default: row - whiteList['flex-flow'] = false; // default: depending on individual properties - whiteList['flex-grow'] = false; // default: 0 - whiteList['flex-shrink'] = false; // default: 1 - whiteList['flex-wrap'] = false; // default: nowrap - whiteList['float'] = false; // default: none - whiteList['float-offset'] = false; // default: 0 0 - whiteList['flood-color'] = false; // default: black - whiteList['flood-opacity'] = false; // default: 1 - whiteList['flow-from'] = false; // default: none - whiteList['flow-into'] = false; // default: none - whiteList['font'] = true; // default: depending on individual properties - whiteList['font-family'] = true; // default: implementation dependent - whiteList['font-feature-settings'] = true; // default: normal - whiteList['font-kerning'] = true; // default: auto - whiteList['font-language-override'] = true; // default: normal - whiteList['font-size'] = true; // default: medium - whiteList['font-size-adjust'] = true; // default: none - whiteList['font-stretch'] = true; // default: normal - whiteList['font-style'] = true; // default: normal - whiteList['font-synthesis'] = true; // default: weight style - whiteList['font-variant'] = true; // default: normal - whiteList['font-variant-alternates'] = true; // default: normal - whiteList['font-variant-caps'] = true; // default: normal - whiteList['font-variant-east-asian'] = true; // default: normal - whiteList['font-variant-ligatures'] = true; // default: normal - whiteList['font-variant-numeric'] = true; // default: normal - whiteList['font-variant-position'] = true; // default: normal - whiteList['font-weight'] = true; // default: normal - whiteList['grid'] = false; // default: depending on individual properties - whiteList['grid-area'] = false; // default: depending on individual properties - whiteList['grid-auto-columns'] = false; // default: auto - whiteList['grid-auto-flow'] = false; // default: none - whiteList['grid-auto-rows'] = false; // default: auto - whiteList['grid-column'] = false; // default: depending on individual properties - whiteList['grid-column-end'] = false; // default: auto - whiteList['grid-column-start'] = false; // default: auto - whiteList['grid-row'] = false; // default: depending on individual properties - whiteList['grid-row-end'] = false; // default: auto - whiteList['grid-row-start'] = false; // default: auto - whiteList['grid-template'] = false; // default: depending on individual properties - whiteList['grid-template-areas'] = false; // default: none - whiteList['grid-template-columns'] = false; // default: none - whiteList['grid-template-rows'] = false; // default: none - whiteList['hanging-punctuation'] = false; // default: none - whiteList['height'] = true; // default: auto - whiteList['hyphens'] = false; // default: manual - whiteList['icon'] = false; // default: auto - whiteList['image-orientation'] = false; // default: auto - whiteList['image-resolution'] = false; // default: normal - whiteList['ime-mode'] = false; // default: auto - whiteList['initial-letters'] = false; // default: normal - whiteList['inline-box-align'] = false; // default: last - whiteList['justify-content'] = false; // default: auto - whiteList['justify-items'] = false; // default: auto - whiteList['justify-self'] = false; // default: auto - whiteList['left'] = false; // default: auto - whiteList['letter-spacing'] = true; // default: normal - whiteList['lighting-color'] = true; // default: white - whiteList['line-box-contain'] = false; // default: block inline replaced - whiteList['line-break'] = false; // default: auto - whiteList['line-grid'] = false; // default: match-parent - whiteList['line-height'] = false; // default: normal - whiteList['line-snap'] = false; // default: none - whiteList['line-stacking'] = false; // default: depending on individual properties - whiteList['line-stacking-ruby'] = false; // default: exclude-ruby - whiteList['line-stacking-shift'] = false; // default: consider-shifts - whiteList['line-stacking-strategy'] = false; // default: inline-line-height - whiteList['list-style'] = true; // default: depending on individual properties - whiteList['list-style-image'] = true; // default: none - whiteList['list-style-position'] = true; // default: outside - whiteList['list-style-type'] = true; // default: disc - whiteList['margin'] = true; // default: depending on individual properties - whiteList['margin-bottom'] = true; // default: 0 - whiteList['margin-left'] = true; // default: 0 - whiteList['margin-right'] = true; // default: 0 - whiteList['margin-top'] = true; // default: 0 - whiteList['marker-offset'] = false; // default: auto - whiteList['marker-side'] = false; // default: list-item - whiteList['marks'] = false; // default: none - whiteList['mask'] = false; // default: border-box - whiteList['mask-box'] = false; // default: see individual properties - whiteList['mask-box-outset'] = false; // default: 0 - whiteList['mask-box-repeat'] = false; // default: stretch - whiteList['mask-box-slice'] = false; // default: 0 fill - whiteList['mask-box-source'] = false; // default: none - whiteList['mask-box-width'] = false; // default: auto - whiteList['mask-clip'] = false; // default: border-box - whiteList['mask-image'] = false; // default: none - whiteList['mask-origin'] = false; // default: border-box - whiteList['mask-position'] = false; // default: center - whiteList['mask-repeat'] = false; // default: no-repeat - whiteList['mask-size'] = false; // default: border-box - whiteList['mask-source-type'] = false; // default: auto - whiteList['mask-type'] = false; // default: luminance - whiteList['max-height'] = true; // default: none - whiteList['max-lines'] = false; // default: none - whiteList['max-width'] = true; // default: none - whiteList['min-height'] = true; // default: 0 - whiteList['min-width'] = true; // default: 0 - whiteList['move-to'] = false; // default: normal - whiteList['nav-down'] = false; // default: auto - whiteList['nav-index'] = false; // default: auto - whiteList['nav-left'] = false; // default: auto - whiteList['nav-right'] = false; // default: auto - whiteList['nav-up'] = false; // default: auto - whiteList['object-fit'] = false; // default: fill - whiteList['object-position'] = false; // default: 50% 50% - whiteList['opacity'] = false; // default: 1 - whiteList['order'] = false; // default: 0 - whiteList['orphans'] = false; // default: 2 - whiteList['outline'] = false; // default: depending on individual properties - whiteList['outline-color'] = false; // default: invert - whiteList['outline-offset'] = false; // default: 0 - whiteList['outline-style'] = false; // default: none - whiteList['outline-width'] = false; // default: medium - whiteList['overflow'] = false; // default: depending on individual properties - whiteList['overflow-wrap'] = false; // default: normal - whiteList['overflow-x'] = false; // default: visible - whiteList['overflow-y'] = false; // default: visible - whiteList['padding'] = true; // default: depending on individual properties - whiteList['padding-bottom'] = true; // default: 0 - whiteList['padding-left'] = true; // default: 0 - whiteList['padding-right'] = true; // default: 0 - whiteList['padding-top'] = true; // default: 0 - whiteList['page'] = false; // default: auto - whiteList['page-break-after'] = false; // default: auto - whiteList['page-break-before'] = false; // default: auto - whiteList['page-break-inside'] = false; // default: auto - whiteList['page-policy'] = false; // default: start - whiteList['pause'] = false; // default: implementation dependent - whiteList['pause-after'] = false; // default: implementation dependent - whiteList['pause-before'] = false; // default: implementation dependent - whiteList['perspective'] = false; // default: none - whiteList['perspective-origin'] = false; // default: 50% 50% - whiteList['pitch'] = false; // default: medium - whiteList['pitch-range'] = false; // default: 50 - whiteList['play-during'] = false; // default: auto - whiteList['position'] = false; // default: static - whiteList['presentation-level'] = false; // default: 0 - whiteList['quotes'] = false; // default: text - whiteList['region-fragment'] = false; // default: auto - whiteList['resize'] = false; // default: none - whiteList['rest'] = false; // default: depending on individual properties - whiteList['rest-after'] = false; // default: none - whiteList['rest-before'] = false; // default: none - whiteList['richness'] = false; // default: 50 - whiteList['right'] = false; // default: auto - whiteList['rotation'] = false; // default: 0 - whiteList['rotation-point'] = false; // default: 50% 50% - whiteList['ruby-align'] = false; // default: auto - whiteList['ruby-merge'] = false; // default: separate - whiteList['ruby-position'] = false; // default: before - whiteList['shape-image-threshold'] = false; // default: 0.0 - whiteList['shape-outside'] = false; // default: none - whiteList['shape-margin'] = false; // default: 0 - whiteList['size'] = false; // default: auto - whiteList['speak'] = false; // default: auto - whiteList['speak-as'] = false; // default: normal - whiteList['speak-header'] = false; // default: once - whiteList['speak-numeral'] = false; // default: continuous - whiteList['speak-punctuation'] = false; // default: none - whiteList['speech-rate'] = false; // default: medium - whiteList['stress'] = false; // default: 50 - whiteList['string-set'] = false; // default: none - whiteList['tab-size'] = false; // default: 8 - whiteList['table-layout'] = false; // default: auto - whiteList['text-align'] = true; // default: start - whiteList['text-align-last'] = true; // default: auto - whiteList['text-combine-upright'] = true; // default: none - whiteList['text-decoration'] = true; // default: none - whiteList['text-decoration-color'] = true; // default: currentColor - whiteList['text-decoration-line'] = true; // default: none - whiteList['text-decoration-skip'] = true; // default: objects - whiteList['text-decoration-style'] = true; // default: solid - whiteList['text-emphasis'] = true; // default: depending on individual properties - whiteList['text-emphasis-color'] = true; // default: currentColor - whiteList['text-emphasis-position'] = true; // default: over right - whiteList['text-emphasis-style'] = true; // default: none - whiteList['text-height'] = true; // default: auto - whiteList['text-indent'] = true; // default: 0 - whiteList['text-justify'] = true; // default: auto - whiteList['text-orientation'] = true; // default: mixed - whiteList['text-overflow'] = true; // default: clip - whiteList['text-shadow'] = true; // default: none - whiteList['text-space-collapse'] = true; // default: collapse - whiteList['text-transform'] = true; // default: none - whiteList['text-underline-position'] = true; // default: auto - whiteList['text-wrap'] = true; // default: normal - whiteList['top'] = false; // default: auto - whiteList['transform'] = false; // default: none - whiteList['transform-origin'] = false; // default: 50% 50% 0 - whiteList['transform-style'] = false; // default: flat - whiteList['transition'] = false; // default: depending on individual properties - whiteList['transition-delay'] = false; // default: 0s - whiteList['transition-duration'] = false; // default: 0s - whiteList['transition-property'] = false; // default: all - whiteList['transition-timing-function'] = false; // default: ease - whiteList['unicode-bidi'] = false; // default: normal - whiteList['vertical-align'] = false; // default: baseline - whiteList['visibility'] = false; // default: visible - whiteList['voice-balance'] = false; // default: center - whiteList['voice-duration'] = false; // default: auto - whiteList['voice-family'] = false; // default: implementation dependent - whiteList['voice-pitch'] = false; // default: medium - whiteList['voice-range'] = false; // default: medium - whiteList['voice-rate'] = false; // default: normal - whiteList['voice-stress'] = false; // default: normal - whiteList['voice-volume'] = false; // default: medium - whiteList['volume'] = false; // default: medium - whiteList['white-space'] = false; // default: normal - whiteList['widows'] = false; // default: 2 - whiteList['width'] = true; // default: auto - whiteList['will-change'] = false; // default: auto - whiteList['word-break'] = true; // default: normal - whiteList['word-spacing'] = true; // default: normal - whiteList['word-wrap'] = true; // default: normal - whiteList['wrap-flow'] = false; // default: auto - whiteList['wrap-through'] = false; // default: wrap - whiteList['writing-mode'] = false; // default: horizontal-tb - whiteList['z-index'] = false; // default: auto - - return whiteList; -} - - -/** - * 创建CSS过滤器 - * - * @param {Object} options - * - {Object} whiteList - * - {Function} onAttr - * - {Function} onIgnoreAttr - * - {Function} safeAttrValue - */ -function FilterCSS (options) { - options = shallowCopyObject(options || {}); - options.whiteList = options.whiteList || getDefaultWhiteList() - options.onAttr = options.onAttr || onAttr; - options.onIgnoreAttr = options.onIgnoreAttr || onIgnoreAttr; - options.safeAttrValue = options.safeAttrValue || safeAttrValue; - this.options = options; -} - -FilterCSS.prototype.process = function (css) { - // 兼容各种奇葩输入 - css = css || ''; - css = css.toString(); - if (!css) return ''; - - var me = this; - var options = me.options; - var whiteList = options.whiteList; - var onAttr = options.onAttr; - var onIgnoreAttr = options.onIgnoreAttr; - var safeAttrValue = options.safeAttrValue; - - var retCSS = parseStyle(css, function (sourcePosition, position, name, value, source) { - - var check = whiteList[name]; - var isWhite = false; - if (check === true) isWhite = check; - else if (typeof check === 'function') isWhite = check(value); - else if (check instanceof RegExp) isWhite = check.test(value); - if (isWhite !== true) isWhite = false; - - // 如果过滤后 value 为空则直接忽略 - value = safeAttrValue(name, value); - if (!value) return; - - var opts = { - position: position, - sourcePosition: sourcePosition, - source: source, - isWhite: isWhite - }; - - if (isWhite) { - - var ret = onAttr(name, value, opts); - if (isNull(ret)) { - return name + ':' + value; - } else { - return ret; - } - - } else { - - var ret = onIgnoreAttr(name, value, opts); - if (!isNull(ret)) { - return ret; - } - - } - }); - - return retCSS; -}; - -/** - * default onTag function - * - * @param {String} tag - * @param {String} html - * @param {Object} options - * @return {String} - */ -function onTag(tag, html, options) { - // do nothing -} - -/** - * default onIgnoreTag function - * - * @param {String} tag - * @param {String} html - * @param {Object} options - * @return {String} - */ -function onIgnoreTag(tag, html, options) { - // do nothing -} - -/** - * default onTagAttr function - * - * @param {String} tag - * @param {String} name - * @param {String} value - * @return {String} - */ -function onTagAttr(tag, name, value) { - // do nothing -} - -/** - * default onIgnoreTagAttr function - * - * @param {String} tag - * @param {String} name - * @param {String} value - * @return {String} - */ -function onIgnoreTagAttr(tag, name, value) { - // do nothing -} - -/** - * default escapeHtml function - * - * @param {String} html - */ -function escapeHtml(html) { - return html.replace(REGEXP_LT, "<").replace(REGEXP_GT, ">"); -} - -/** - * default safeAttrValue function - * - * @param {String} tag - * @param {String} name - * @param {String} value - * @param {Object} cssFilter - * @return {String} - */ -function safeAttrValue(tag, name, value, cssFilter) { - // unescape attribute value firstly - value = friendlyAttrValue(value); - - if (name === "href" || name === "src") { - // filter `href` and `src` attribute - // only allow the value that starts with `http://` | `https://` | `mailto:` | `/` | `#` - value = _.trim(value); - if (value === "#") return "#"; - if ( - !( - value.substr(0, 7) === "http://" || - value.substr(0, 8) === "https://" || - value.substr(0, 7) === "mailto:" || - value.substr(0, 4) === "tel:" || - value.substr(0, 11) === "data:image/" || - value.substr(0, 6) === "ftp://" || - value.substr(0, 2) === "./" || - value.substr(0, 3) === "../" || - value[0] === "#" || - value[0] === "/" - ) - ) { - return ""; - } - } else if (name === "background") { - // filter `background` attribute (maybe no use) - // `javascript:` - REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0; - if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) { - return ""; - } - } else if (name === "style") { - // `expression()` - REGEXP_DEFAULT_ON_TAG_ATTR_7.lastIndex = 0; - if (REGEXP_DEFAULT_ON_TAG_ATTR_7.test(value)) { - return ""; - } - // `url()` - REGEXP_DEFAULT_ON_TAG_ATTR_8.lastIndex = 0; - if (REGEXP_DEFAULT_ON_TAG_ATTR_8.test(value)) { - REGEXP_DEFAULT_ON_TAG_ATTR_4.lastIndex = 0; - if (REGEXP_DEFAULT_ON_TAG_ATTR_4.test(value)) { - return ""; - } - } - if (cssFilter !== false) { - cssFilter = cssFilter || defaultCSSFilter; - value = cssFilter.process(value); - } - } - - // escape `<>"` before returns - value = escapeAttrValue(value); - return value; -} - -// RegExp list -var REGEXP_LT = //g; -var REGEXP_QUOTE = /"/g; -var REGEXP_QUOTE_2 = /"/g; -var REGEXP_ATTR_VALUE_1 = /&#([a-zA-Z0-9]*);?/gim; -var REGEXP_ATTR_VALUE_COLON = /:?/gim; -var REGEXP_ATTR_VALUE_NEWLINE = /&newline;?/gim; -var REGEXP_DEFAULT_ON_TAG_ATTR_3 = /\/\*|\*\//gm; -var REGEXP_DEFAULT_ON_TAG_ATTR_4 = /((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi; -var REGEXP_DEFAULT_ON_TAG_ATTR_5 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:/gi; -var REGEXP_DEFAULT_ON_TAG_ATTR_6 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:\s*image\//gi; -var REGEXP_DEFAULT_ON_TAG_ATTR_7 = /e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi; -var REGEXP_DEFAULT_ON_TAG_ATTR_8 = /u\s*r\s*l\s*\(.*/gi; - -/** - * escape doube quote - * - * @param {String} str - * @return {String} str - */ -function escapeQuote(str) { - return str.replace(REGEXP_QUOTE, """); -} - -/** - * unescape double quote - * - * @param {String} str - * @return {String} str - */ -function unescapeQuote(str) { - return str.replace(REGEXP_QUOTE_2, '"'); -} - -/** - * escape html entities - * - * @param {String} str - * @return {String} - */ -function escapeHtmlEntities(str) { - return str.replace(REGEXP_ATTR_VALUE_1, function replaceUnicode(str, code) { - return code[0] === "x" || code[0] === "X" - ? String.fromCharCode(parseInt(code.substr(1), 16)) - : String.fromCharCode(parseInt(code, 10)); - }); -} - -/** - * escape html5 new danger entities - * - * @param {String} str - * @return {String} - */ -function escapeDangerHtml5Entities(str) { - return str - .replace(REGEXP_ATTR_VALUE_COLON, ":") - .replace(REGEXP_ATTR_VALUE_NEWLINE, " "); -} - -/** - * clear nonprintable characters - * - * @param {String} str - * @return {String} - */ -function clearNonPrintableCharacter(str) { - var str2 = ""; - for (var i = 0, len = str.length; i < len; i++) { - str2 += str.charCodeAt(i) < 32 ? " " : str.charAt(i); - } - return _.trim(str2); -} - -/** - * get friendly attribute value - * - * @param {String} str - * @return {String} - */ -function friendlyAttrValue(str) { - str = unescapeQuote(str); - str = escapeHtmlEntities(str); - str = escapeDangerHtml5Entities(str); - str = clearNonPrintableCharacter(str); - return str; -} - -/** - * unescape attribute value - * - * @param {String} str - * @return {String} - */ -function escapeAttrValue(str) { - str = escapeQuote(str); - str = escapeHtml(str); - return str; -} - -/** - * `onIgnoreTag` function for removing all the tags that are not in whitelist - */ -function onIgnoreTagStripAll() { - return ""; -} - -/** - * remove tag body - * specify a `tags` list, if the tag is not in the `tags` list then process by the specify function (optional) - * - * @param {array} tags - * @param {function} next - */ -function StripTagBody(tags, next) { - if (typeof next !== "function") { - next = function() {}; - } - - var isRemoveAllTag = !Array.isArray(tags); - function isRemoveTag(tag) { - if (isRemoveAllTag) return true; - return _.indexOf(tags, tag) !== -1; - } - - var removeList = []; - var posStart = false; - - return { - onIgnoreTag: function(tag, html, options) { - if (isRemoveTag(tag)) { - if (options.isClosing) { - var ret = "[/removed]"; - var end = options.position + ret.length; - removeList.push([ - posStart !== false ? posStart : options.position, - end - ]); - posStart = false; - return ret; - } else { - if (!posStart) { - posStart = options.position; - } - return "[removed]"; - } - } else { - return next(tag, html, options); - } - }, - remove: function(html) { - var rethtml = ""; - var lastPos = 0; - _.forEach(removeList, function(pos) { - rethtml += html.slice(lastPos, pos[0]); - lastPos = pos[1]; - }); - rethtml += html.slice(lastPos); - return rethtml; - } - }; -} - -/** - * remove html comments - * - * @param {String} html - * @return {String} - */ -function stripCommentTag(html) { - return html.replace(STRIP_COMMENT_TAG_REGEXP, ""); -} -var STRIP_COMMENT_TAG_REGEXP = //g; - -/** - * remove invisible characters - * - * @param {String} html - * @return {String} - */ -function stripBlankChar(html) { - var chars = html.split(""); - chars = chars.filter(function(char) { - var c = char.charCodeAt(0); - if (c === 127) return false; - if (c <= 31) { - if (c === 10 || c === 13) return true; - return false; - } - return true; - }); - return chars.join(""); -} - - -/** - * get tag name - * - * @param {String} html e.g. '
' - * @return {String} - */ -function getTagName(html) { - var i = _.spaceIndex(html); - if (i === -1) { - var tagName = html.slice(1, -1); - } else { - var tagName = html.slice(1, i + 1); - } - tagName = _.trim(tagName).toLowerCase(); - if (tagName.slice(0, 1) === "/") tagName = tagName.slice(1); - if (tagName.slice(-1) === "/") tagName = tagName.slice(0, -1); - return tagName; -} - -/** - * is close tag? - * - * @param {String} html 如:'' - * @return {Boolean} - */ -function isClosing(html) { - return html.slice(0, 2) === "") { - rethtml += escapeHtml(html.slice(lastPos, tagStart)); - currentHtml = html.slice(tagStart, currentPos + 1); - currentTagName = getTagName(currentHtml); - rethtml += onTag( - tagStart, - rethtml.length, - currentTagName, - currentHtml, - isClosing(currentHtml) - ); - lastPos = currentPos + 1; - tagStart = false; - continue; - } - if ((c === '"' || c === "'") && html.charAt(currentPos - 1) === "=") { - quoteStart = c; - continue; - } - } else { - if (c === quoteStart) { - quoteStart = false; - continue; - } - } - } - } - if (lastPos < html.length) { - rethtml += escapeHtml(html.substr(lastPos)); - } - - return rethtml; -} - -var REGEXP_ILLEGAL_ATTR_NAME = /[^a-zA-Z0-9_:\.\-]/gim; - -/** - * parse input attributes and returns processed attributes - * - * @param {String} html e.g. `href="#" target="_blank"` - * @param {Function} onAttr e.g. `function (name, value)` - * @return {String} - */ -function parseAttr(html, onAttr) { - "use strict"; - - var lastPos = 0; - var retAttrs = []; - var tmpName = false; - var len = html.length; - - function addAttr(name, value) { - name = _.trim(name); - name = name.replace(REGEXP_ILLEGAL_ATTR_NAME, "").toLowerCase(); - if (name.length < 1) return; - var ret = onAttr(name, value || ""); - if (ret) retAttrs.push(ret); - } - - // 逐个分析字符 - for (var i = 0; i < len; i++) { - var c = html.charAt(i); - var v, j; - if (tmpName === false && c === "=") { - tmpName = html.slice(lastPos, i); - lastPos = i + 1; - continue; - } - if (tmpName !== false) { - if ( - i === lastPos && - (c === '"' || c === "'") && - html.charAt(i - 1) === "=" - ) { - j = html.indexOf(c, i + 1); - if (j === -1) { - break; - } else { - v = _.trim(html.slice(lastPos + 1, j)); - addAttr(tmpName, v); - tmpName = false; - i = j; - lastPos = i + 1; - continue; - } - } - } - if (/\s|\n|\t/.test(c)) { - html = html.replace(/\s|\n|\t/g, " "); - if (tmpName === false) { - j = findNextEqual(html, i); - if (j === -1) { - v = _.trim(html.slice(lastPos, i)); - addAttr(v); - tmpName = false; - lastPos = i + 1; - continue; - } else { - i = j - 1; - continue; - } - } else { - j = findBeforeEqual(html, i - 1); - if (j === -1) { - v = _.trim(html.slice(lastPos, i)); - v = stripQuoteWrap(v); - addAttr(tmpName, v); - tmpName = false; - lastPos = i + 1; - continue; - } else { - continue; - } - } - } - } - - if (lastPos < html.length) { - if (tmpName === false) { - addAttr(html.slice(lastPos)); - } else { - addAttr(tmpName, stripQuoteWrap(_.trim(html.slice(lastPos)))); - } - } - - return _.trim(retAttrs.join(" ")); -} - -function findNextEqual(str, i) { - for (; i < str.length; i++) { - var c = str[i]; - if (c === " ") continue; - if (c === "=") return i; - return -1; - } -} - -function findBeforeEqual(str, i) { - for (; i > 0; i--) { - var c = str[i]; - if (c === " ") continue; - if (c === "=") return i; - return -1; - } -} - -function isQuoteWrapString(text) { - if ( - (text[0] === '"' && text[text.length - 1] === '"') || - (text[0] === "'" && text[text.length - 1] === "'") - ) { - return true; - } else { - return false; - } -} - -function stripQuoteWrap(text) { - if (isQuoteWrapString(text)) { - return text.substr(1, text.length - 2); - } else { - return text; - } -} - - -/** - * returns `true` if the input value is `undefined` or `null` - * - * @param {Object} obj - * @return {Boolean} - */ -function isNull(obj) { - return obj === undefined || obj === null; -} - -/** - * get attributes for a tag - * - * @param {String} html - * @return {Object} - * - {String} html - * - {Boolean} closing - */ -function getAttrs(html) { - var i = _.spaceIndex(html); - if (i === -1) { - return { - html: "", - closing: html[html.length - 2] === "/" - }; - } - html = _.trim(html.slice(i + 1, -1)); - var isClosing = html[html.length - 1] === "/"; - if (isClosing) html = _.trim(html.slice(0, -1)); - return { - html: html, - closing: isClosing - }; -} - diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index ea3bffb6581..df16e5c70e8 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -365,6 +365,7 @@ QtObject { readonly property string storeToKeychainValueNotNow: "notNow" readonly property string storeToKeychainValueNever: "never" + // WARNING: Remove later. Moved to StatusQ. readonly property string editLabel: ` ` + qsTr("(edited)") + `` readonly property string newBookmark: " " diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index af595debafb..6944d9a39dd 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -52,55 +52,6 @@ QtObject { return Style.current.accountColors[colorIndex] } - function getMessageWithStyle(msg, isCurrentUser, hoveredLink = "") { - return `` + - `${msg}` - } - function getReplyMessageStyle(msg, isCurrentUser) { return `