From fbeb3e8e484b49372c692fb9c35779072205bf43 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Tue, 5 Jul 2022 14:12:27 +0400 Subject: [PATCH] fix(ChatMessagesView): Usage of `StatusMessage` WIP --- .../modules/main/activity_center/module.nim | 5 +- .../chat_content/messages/module.nim | 14 +- .../main/chat_section/chat_content/module.nim | 8 +- .../chat_content/users/module.nim | 6 +- .../profile_section/notifications/module.nim | 2 +- .../modules/shared_models/message_item.nim | 15 +- .../shared_models/message_item_qobject.nim | 4 + .../modules/shared_models/message_model.nim | 16 +- ui/StatusQ | 2 +- .../controls/activityCenter/ChannelBadge.qml | 4 +- .../activityCenter/ReplyComponent.qml | 2 +- .../Chat/popups/PinnedMessagesPopup.qml | 4 +- .../popups/community/CreateCategoryPopup.qml | 5 +- .../popups/community/CreateChannelPopup.qml | 8 +- .../AppLayouts/Chat/stores/MessageStore.qml | 2 + .../ActivityCenterMessageComponentView.qml | 6 +- .../AppLayouts/Chat/views/ChatContentView.qml | 17 +- .../Chat/views/ChatMessagesView.qml | 178 ++- .../Chat/views/CommunitySettingsView.qml | 9 +- .../popups/CreateCommunityPopup.qml | 9 +- .../Profile/views/AppearanceView.qml | 2 +- .../shared/panels/chat/ChatReplyPanel.qml | 8 +- .../ContactVerificationRequestPopup.qml | 4 +- ui/imports/shared/qmldir | 1 - ui/imports/shared/status/StatusChatInput.qml | 986 ++++++------ .../status/StatusChatInputReplyArea.qml | 2 +- ui/imports/shared/views/ProfileView.qml | 4 +- ui/imports/shared/views/chat/ChatTextView.qml | 10 +- .../shared/views/chat/CompactMessageView.qml | 80 +- .../views/chat/MessageContextMenuView.qml | 8 +- ui/imports/shared/views/chat/MessageView.qml | 569 ++++++- ui/imports/shared/xss.js | 1318 ----------------- ui/imports/utils/Constants.qml | 1 + ui/imports/utils/Utils.qml | 17 +- 34 files changed, 1244 insertions(+), 2082 deletions(-) delete mode 100644 ui/imports/shared/xss.js 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..3410485b852 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 @@ -94,7 +94,8 @@ proc createFetchMoreMessagesItem(self: Module): Item = @[], newTransactionParametersItem("","","","","","",-1,""), @[], - TrustStatus.Unknown + TrustStatus.Unknown, + false ) proc createChatIdentifierItem(self: Module): Item = @@ -131,7 +132,8 @@ proc createChatIdentifierItem(self: Module): Item = @[], newTransactionParametersItem("","","","","","",-1,""), @[], - TrustStatus.Unknown + TrustStatus.Unknown, + 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..4120b85067a 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 @@ -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 9de0e8ffc5a..ecd7cafd308 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit 9de0e8ffc5a7394a6bcf2f8d2e3ad21afa70b97f +Subproject commit ecd7cafd30839a4b3335ae6dd1e22fd25e40807a diff --git a/ui/app/AppLayouts/Chat/controls/activityCenter/ChannelBadge.qml b/ui/app/AppLayouts/Chat/controls/activityCenter/ChannelBadge.qml index b9165065159..876a1ee1cd1 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.filterXSS(name))) : + "#" + StatusQUtils.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/popups/PinnedMessagesPopup.qml b/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml index d7da4e06828..3cf886c0390 100644 --- a/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/PinnedMessagesPopup.qml @@ -104,7 +104,7 @@ ModalPopup { MessageView { id: messageItem - store: popup.store + rootStore: popup.store messageStore: popup.messageStore messageContextMenu: msgContextMenu @@ -115,7 +115,7 @@ ModalPopup { senderLocalName: model.senderLocalName senderIcon: model.senderIcon amISender: model.amISender - message: model.messageText + messageText: model.messageText messageImage: model.messageImage messageTimestamp: model.timestamp messageOutgoingStatus: model.outgoingStatus diff --git a/ui/app/AppLayouts/Chat/popups/community/CreateCategoryPopup.qml b/ui/app/AppLayouts/Chat/popups/community/CreateCategoryPopup.qml index 8c7b06aabdf..2f3a9cbb87b 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.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.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 038b0d85dfc..2b357c1dc44 100644 --- a/ui/app/AppLayouts/Chat/popups/community/CreateChannelPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/community/CreateChannelPopup.qml @@ -298,14 +298,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.filterXSS(popup.contentItem.channelName.input.text), + StatusQUtils.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.filterXSS(popup.contentItem.channelName.input.text), + StatusQUtils.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..839e23ec5b5 100644 --- a/ui/app/AppLayouts/Chat/stores/MessageStore.qml +++ b/ui/app/AppLayouts/Chat/stores/MessageStore.qml @@ -135,10 +135,12 @@ QtObject { return messageModule.toggleReaction(messageId, emojiId) } + // TODO: Remove. Moved to StatusQ. function lastTwoItems(nodes) { return nodes.join(qsTr(" and ")); } + // TODO: Remove. Moved to StatusQ. function showReactionAuthors(jsonArrayOfUsersReactedWithThisEmoji, emojiId) { let listOfUsers = JSON.parse(jsonArrayOfUsersReactedWithThisEmoji) if (listOfUsers.error) { diff --git a/ui/app/AppLayouts/Chat/views/ActivityCenterMessageComponentView.qml b/ui/app/AppLayouts/Chat/views/ActivityCenterMessageComponentView.qml index da99b2045eb..2b189d3ce0c 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 @@ -143,7 +143,7 @@ Item { read: 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 ed8ef3a1a34..4b8e9dc1966 100644 --- a/ui/app/AppLayouts/Chat/views/ChatContentView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatContentView.qml @@ -93,7 +93,7 @@ ColumnLayout { if(cnt > 1) return qsTr("%1 members").arg(cnt); return qsTr("1 member"); case Constants.chatType.communityChat: - return Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim() + return StatusQUtils.Utils.linkifyAndXSS(chatContentModule.chatDetails.description).trim() default: return "" } @@ -159,10 +159,11 @@ ColumnLayout { Component { id: contactsSelector GroupChatPanel { - sectionModule: root.chatSectionModule + sectionModule: chatSectionModule chatContentModule: root.chatContentModule rootStore: root.rootStore maxHeight: root.height + onPanelClosed: topBar.toolbarComponent = statusChatInfoButton } } @@ -425,9 +426,10 @@ 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 @@ -452,8 +454,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 @@ -470,6 +471,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 7e607a282ea..b926e8938c3 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 { @@ -97,20 +103,6 @@ Item { StatusListView { id: chatLogView - anchors.top: loadingMessagesIndicator.bottom - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - 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) { @@ -124,30 +116,78 @@ Item { } } + anchors.top: loadingMessagesIndicator.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + verticalLayoutDirection: ListView.BottomToTop + + 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 + + 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 + } + visible: false height: 32 width: nbMessages.width + arrowImage.width + 2 * Style.current.halfPadding + (nbMessages.visible ? scrollDownButton.buttonPadding : 0) @@ -159,6 +199,7 @@ Item { border.width: 0 radius: 16 } + onClicked: { newMessages = 0 scrollDownButton.visible = false @@ -201,50 +242,23 @@ 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) -// } -// } + // target: root.rootStore.chatsModelInst - onContentYChanged: { - scrollDownButton.visible = contentHeight - (scrollY + height) > 400 - let loadMore = scrollDownButton.visible && scrollY < 500 - if(loadMore){ - messageStore.loadMoreMessages() - } - } - - model: messageStore.messagesModel - - Component.onCompleted: chatLogView.scrollToBottom(true) + // onAppReady: { + // chatLogView.scrollToBottom(true) + // } + // } delegate: MessageView { id: msgDelegate - store: root.store + width: ListView.view.width + + newComponentEnabled: messageComponentSwitch.checked + + rootStore: root.rootStore messageStore: root.messageStore usersStore: root.usersStore contactsStore: root.contactsStore @@ -253,7 +267,7 @@ Item { isActiveChannel: root.isActiveChannel isChatBlocked: root.isChatBlocked - messageContextMenu: messageContextMenuInst + messageContextMenu: root.messageContextMenu itemIndex: index messageId: model.id @@ -262,10 +276,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 @@ -301,7 +316,7 @@ Item { root.showReplyArea(messageId, author) } - onImageClicked: Global.openImagePopup(image, messageContextMenuInst) + onImageClicked: Global.openImagePopup(image, messageContextMenu) // ✅ stickersLoaded: root.stickersLoaded @@ -312,6 +327,17 @@ Item { } } + StatusSwitch { + id: messageComponentSwitch + anchors { + top: parent.top + right: parent.right + margins: 10 + + } + text: "Use StatusMessage" + } + MessageDialog { id: sendingMsgFailedPopup standardButtons: StandardButton.Ok diff --git a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml index 7b07426662e..55f7dc2e4fd 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.filterXSS(item.name), + StatusQUtils.filterXSS(item.description), + StatusQUtils.filterXSS(item.introMessage), + StatusQUtils.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 0b549d01aa0..4107de04fe5 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.filterXSS(nameInput.input.text), + description: StatusQUtils.filterXSS(descriptionTextInput.input.text), + introMessage: StatusQUtils.filterXSS(introMessageInput.input.text), + outroMessage: StatusQUtils.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 c10d55ffbad..e586b1f8f24 100644 --- a/ui/app/AppLayouts/Profile/views/AppearanceView.qml +++ b/ui/app/AppLayouts/Profile/views/AppearanceView.qml @@ -88,7 +88,7 @@ SettingsContentBase { 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/imports/shared/panels/chat/ChatReplyPanel.qml b/ui/imports/shared/panels/chat/ChatReplyPanel.qml index d52096e7d3e..5987696ee3a 100644 --- a/ui/imports/shared/panels/chat/ChatReplyPanel.qml +++ b/ui/imports/shared/panels/chat/ChatReplyPanel.qml @@ -34,7 +34,7 @@ Loader { property int chatHorizontalPadding property string stickerData - signal clickMessage(bool isProfileClick, bool isSticker, bool isImage, var image, bool isEmoji, bool hideEmojiPicker, bool isReply) + signal clickMessage(bool isProfileClick, bool isSticker, bool isImage, var image, bool isEmoji, bool hideEmojiPicker) signal scrollToBottom(bool isit, var container) sourceComponent: Component { @@ -110,7 +110,7 @@ Loader { pubkey: repliedMessageSenderPubkey image: repliedMessageSenderIcon - onClicked: root.clickMessage(true, false, false, null, false, false, true) + onClicked: root.clickMessage(true, false, false, null, false, false) } StyledTextEdit { @@ -169,9 +169,9 @@ Loader { 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) + return Utils.getReplyMessageStyle(StatusQUtils.Emoji.parse(StatusQUtils.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) + return Utils.getReplyMessageStyle(StatusQUtils.Emoji.parse(StatusQUtils.Utils.linkifyAndXSS(repliedMessageContent), StatusQUtils.Emoji.size.small), amISenderOfTheRepliedMessage) } } textFormat: Text.RichText diff --git a/ui/imports/shared/popups/ContactVerificationRequestPopup.qml b/ui/imports/shared/popups/ContactVerificationRequestPopup.qml index ccea7857873..84109446d46 100644 --- a/ui/imports/shared/popups/ContactVerificationRequestPopup.qml +++ b/ui/imports/shared/popups/ContactVerificationRequestPopup.qml @@ -68,7 +68,7 @@ StatusModal { messageTimestamp: root.messageTimestamp senderDisplayName: root.senderDisplayName senderIcon: root.senderIcon - message: root.challengeText + messageText: root.challengeText messageContentType: Constants.messageContentType.messageType placeholderMessage: true } @@ -97,7 +97,7 @@ StatusModal { 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..e41b85d48e8 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) @@ -76,20 +77,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 @@ -270,7 +301,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 +326,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() { @@ -766,505 +797,512 @@ 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 +// anchors.margins: 8 +// anchors.rightMargin: Style.current.halfPadding + spacing: 4 + + StatusQ.StatusFlatRoundButton { + id: chatCommandsBtn + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignBottom + 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() } - 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 && !control.isStatusUpdateInput + enabled: !control.isContactBlocked + onClicked: { + highlighted = true + imageDialog.open() } } Rectangle { - id: extendedArea - visible: isImage || isReply - height: { - if (visible) { - if (isImage) { - return imageArea.height - } - - if (isReply) { - return replyArea.height + replyArea.anchors.topMargin - } + id: messageInput + Layout.fillWidth: true + enabled: !control.isContactBlocked + + readonly property int maxInputFieldHeight: control.isStatusUpdateInput ? 124 : 112 + readonly property int defaultInputFieldHeight: control.isStatusUpdateInput ? 56 : 40 + + // anchors.left: imageBtn.visible ? imageBtn.right : parent.left + // anchors.right: unblockBtn.visible ? unblockBtn.left : parent.right + // anchors.top: control.isStatusUpdateInput ? parent.top : undefined + // anchors.bottom: !control.isStatusUpdateInput ? parent.bottom : undefined + // anchors.leftMargin: imageBtn.visible ? 5 : Style.current.smallPadding + // anchors.bottomMargin: control.isStatusUpdateInput ? 0 : 12 + // anchors.rightMargin: Style.current.smallPadding + + // implicitHeight: { + // if (messageInputField.implicitHeight <= messageInput.defaultInputFieldHeight) { + // return messageInput.defaultInputFieldHeight + // } + // if (messageInputField.implicitHeight >= messageInput.maxInputFieldHeight) { + // return messageInput.maxInputFieldHeight + // } + // return messageInputField.implicitHeight + // } + + Layout.preferredHeight: Math.min(Math.max(messageInputField.implicitHeight, + messageInputField.implicitHeight), + messageInput.maxInputFieldHeight) + + 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 } - 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 { - color: parent.color - 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 - } + id: extendedArea + visible: isImage || isReply + height: { + if (visible) { + if (isImage) { + return imageArea.height + } - 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) + if (isReply) { + return replyArea.height + replyArea.anchors.topMargin + } + } + 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 { + color: parent.color + 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 + } + + 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) + } + 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 } - isImage = control.fileUrls.length > 0 - validateImages(control.fileUrls) } } - StatusChatInputReplyArea { - id: replyArea - visible: isReply + StatusSmartIdenticon { + id: profileImage anchors.left: parent.left - anchors.leftMargin: 2 - anchors.right: parent.right - anchors.rightMargin: 2 + anchors.leftMargin: Style.current.smallPadding anchors.top: parent.top - anchors.topMargin: 2 - // Not Refactored Yet -// stickerData: sticker - onCloseButtonClicked: { - isReply = false - } + anchors.topMargin: Style.current.halfPadding + image.source: userProfile.icon + visible: control.isStatusUpdateInput } - } - - 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 - } - 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) { + StatusScrollView { + id: scrollView + 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 + 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); + } + } 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_Backspace) || (keyEvent.key === Qt.Key_Delete)) { - messageInputField.remove(mentionsPos[i].rightIndex, mentionsPos[i].leftIndex); - mentionsPos.pop(i); + } 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; + } } - } 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); } } } } - 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); } - } - 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 = []; + + 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 { - var removeFrom = (cursorPosition < messageLimit) ? cursorWhenPressed : messageLimit; - remove(removeFrom, cursorPosition); + lastClick = now } - 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(); + 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(); + } } - lastClick = now } - StatusSyntaxHighlighter { - quickTextDocument: messageInputField.textDocument + Shortcut { + enabled: messageInputField.activeFocus + sequence: StandardKey.Bold + onActivated: wrapSelection("**") } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - enabled: parent.hoveredLink - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor + Shortcut { + enabled: messageInputField.activeFocus + sequence: StandardKey.Italic + onActivated: wrapSelection("*") } - 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: "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 + Rectangle { + color: parent.color anchors.bottom: parent.bottom - icon.name: "image" - type: StatusQ.StatusFlatRoundButton.Type.Tertiary - visible: control.isStatusUpdateInput + anchors.right: parent.right + visible: !control.isStatusUpdateInput + height: parent.height / 2 + width: 32 + radius: Style.current.radius + } - onClicked: { - highlighted = true - imageDialog.open() - } + 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() : "" } - StatusQ.StatusFlatButton { - id: sendBtn - icon.name: "send" - text: qsTr("Send") - size: StatusQ.StatusBaseButton.Size.Small + 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.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(); + 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 + + onClicked: { + highlighted = true + imageDialog.open() + } } - } - 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.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 - 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: 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 - 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.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 - 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() + StatusQ.StatusButton { + id: unblockBtn + visible: control.isContactBlocked + 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..b612149d871 100644 --- a/ui/imports/shared/status/StatusChatInputReplyArea.qml +++ b/ui/imports/shared/status/StatusChatInputReplyArea.qml @@ -61,7 +61,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/views/ProfileView.qml b/ui/imports/shared/views/ProfileView.qml index 51ef1c7c67a..43f7bd41ff2 100644 --- a/ui/imports/shared/views/ProfileView.qml +++ b/ui/imports/shared/views/ProfileView.qml @@ -355,7 +355,7 @@ Rectangle { messageTimestamp: root.verificationRequestedAt senderDisplayName: userProfile.name senderIcon: userProfile.icon - message: root.verificationChallenge + messageText: root.verificationChallenge messageContentType: Constants.messageContentType.messageType placeholderMessage: true } @@ -369,7 +369,7 @@ Rectangle { 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 index d697a6327bd..97d953b99a9 100644 --- a/ui/imports/shared/views/chat/CompactMessageView.qml +++ b/ui/imports/shared/views/chat/CompactMessageView.qml @@ -346,7 +346,7 @@ Item { } onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, image, isEmoji, hideEmojiPicker, isReply, false, "") + root.clickMessage(isProfileClick, isSticker, isImage, image, isEmoji, hideEmojiPicker, true, false, "") } } @@ -426,67 +426,30 @@ Item { suggestionsOpened = false } - StatusChatInput { - id: editTextInput +// StatusChatInput { +// id: editTextInput - store: root.store - usersStore: root.usersStore +// 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 - } - } +// chatType: messageStore.getChatType() +// isEdit: true +// emojiPopup: root.emojiPopup +// messageContextMenu: root.messageContextMenu - 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 - } +// onSendMessage: { +// saveBtn.clicked(null) +// } +// suggestions.onVisibleChanged: { +// if (suggestions.visible) { +// editText.suggestionsOpened = true +// } +// } - 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 - } - } +// Component.onCompleted: { +// parseMessage(message); +// } +// } StatusQControls.StatusFlatButton { id: cancelBtn @@ -700,7 +663,6 @@ Item { } } - Retry { id: retry height: visible ? implicitHeight : 0 diff --git a/ui/imports/shared/views/chat/MessageContextMenuView.qml b/ui/imports/shared/views/chat/MessageContextMenuView.qml index 53c914763d0..453ef51a9a6 100644 --- a/ui/imports/shared/views/chat/MessageContextMenuView.qml +++ b/ui/imports/shared/views/chat/MessageContextMenuView.qml @@ -102,12 +102,12 @@ StatusPopupMenu { d.contactDetails = {} } - onHeightChanged: { root.y = setYPosition(); } - onWidthChanged: { root.x = setXPosition(); } +// onHeightChanged: { root.y = setYPosition(); } +// onWidthChanged: { root.x = setXPosition(); } onOpened: { // Trigger x and y position: - x = setXPosition() - y = setYPosition() +// x = setXPosition() +// y = setYPosition() } width: Math.max(emojiContainer.visible ? emojiContainer.width : 0, 230) diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index 1db2b791c7b..c339490f356 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -1,38 +1,46 @@ 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 + // 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 + 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 root.newComponentEnabled ? newMessageComponent + : compactMessageComponent } } + + property bool newComponentEnabled: false - property var store + property var rootStore property var messageStore property var usersStore property var contactsStore @@ -53,19 +61,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: "" @@ -100,18 +109,19 @@ Loader { } } - // Legacy property string responseTo: responseToMessageWithId + property string timestamp: messageTimestamp + + // Legacy 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 "" } @@ -143,10 +153,11 @@ Loader { 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 @@ -164,15 +175,17 @@ Loader { 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(isProfileClick, + isSticker = false, + isImage = false, + image = null, + isEmoji = false, + hideEmojiPicker = false, + isReply = false, + isRightClickOnImage = false, + imageSource = "") { if (placeholderMessage || activityCenterMessage) { return @@ -187,7 +200,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 @@ -201,7 +214,7 @@ Loader { messageContextMenu.isSticker = isSticker messageContextMenu.hideEmojiPicker = hideEmojiPicker - if(isReply){ + if (isReply){ let obj = messageStore.getMessageByIdAsJson(responseTo) if(!obj) return @@ -212,15 +225,17 @@ Loader { messageContextMenu.selectedUserIcon = obj.senderIcon } - messageContextMenu.popup() + console.log("Opening messageContextMenu: ", messageContextMenu.isProfile, messageContextMenu.imageSource); + + messageContextMenu.popup(4, 4) } 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(); @@ -230,23 +245,43 @@ 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 + // } + // } + // } + // } + + function setMessageActive(messageId, active) { + if (active) { + activeMessage = messageId; + return; + } + if (activeMessage === messageId) { + activeMessage = ""; + return; + } + } + + QtObject { + id: d + + readonly property bool canPin: !!messageStore && + messageStore.getNumberOfPinnedMessages() < Constants.maxNumberOfPins + + readonly property int chatButtonSize: 32 + } Component { id: gapComponent @@ -292,18 +327,18 @@ Loader { wrapMode: Text.Wrap text: { return ``+ - ``+ - ``+ - ``+ - ``+ - `${message}`+ - ``+ - ``; + `}`+ + ``+ + ``+ + ``+ + `${messageText}`+ + ``+ + ``; } visible: isStatusMessage font.pixelSize: 14 @@ -316,13 +351,382 @@ Loader { } } + + Component { + id: newMessageComponent + + 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(model.contentType) + 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: model.timestamp + isAReply: delegate.isReply // TODO: model.isReply + isEdited: root.isEdited + hasMention: model.mentionedUsersPks.split(" ").includes(root.rootStore.userProfileInst.pubKey) + isPinned: model.pinned + pinnedBy: Utils.getContactDetailsAsJson(model.pinnedBy).displayName + hasExpired: false // TODO: model.hasExpired + reactionsModel: root.reactionsModel + + previousMessageIndex: root.prevMessageIndex + previousMessageTimestamp: root.prevMsgTimestamp + + showHeader: root.authorCurrentMsg !== root.authorPrevMsg || + root.shouldRepeatHeader || dateGroupVisible || isAReply + + editMode: root.editModeOn + + onEditCancelled: { + root.messageStore.setEditModeOff(root.messageId) + } + + onEditCompleted: { + delegate.editCompletedHandler(newMsgText) + } + + onImageClicked: { + root.imageClicked(imageSource); + } + + onLinkActivated: { + if (link.startsWith('//')) { + const pubkey = link.replace("//", ""); + Global.openProfilePopup(pubkey) + return; + } + + Global.openLink(link) + } + + onProfilePictureClicked: { + root.messageClickHandler(true); + } + + onSenderNameClicked: { + root.messageClickHandler(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 + + // First set parent, X & Y positions for the messageContextMenu + root.messageContextMenu.parent = this; + 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.messageClickHandler(false, false, false, null, true, false); + } + + messageDetails: StatusMessageDetails { + contentType: delegate.contentType + messageText: root.messageText + messageContent: { + switch (delegate.contentType) + { + case StatusMessage.ContentType.Sticker: + return model.sticker; + case StatusMessage.ContentType.Image: + return model.model.messageImage; + } + return ""; + } + + amISender: !!model.amIsender + sender.id: model.senderId + sender.userName: root.senderDisplayName + sender.localName: root.senderLocalName + sender.ensName: root.senderEnsName + sender.isContact: model.senderIsAdded + sender.trustIndicator: root.senderTrustStatus + sender.profileImage { + width: 40 + height: 40 + pubkey: model.senderId + source: model.senderIcon || "" + colorId: Utils.colorIdForPubkey(model.senderId) + colorHash: Utils.getColorHashAsJson(model.senderId) + } + + } + + 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.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) + } + } + + statusChatInput: StatusChatInput { + id: editTextInput + + readonly property string messageText: editTextInput.textInput.text + + // TODO: Move this property and Escape handler to StatusChatInput + property bool suggestionsOpened: false + + Keys.onEscapePressed: { + if (!suggestionsOpened) { + // cancelBtn.clicked() + delegate.editCancelled() + } + suggestionsOpened = false + } + + store: root.rootStore + usersStore: root.usersStore + emojiPopup: root.emojiPopup + messageContextMenu: root.messageContextMenu + + chatType: root.messageStore.getChatType() + isEdit: true + + onSendMessage: { + // saveBtn.clicked(null) + delegate.editCompletedHandler(editTextInput.textInput.text) + } + + suggestions.onVisibleChanged: { + if (suggestions.visible) { + suggestionsOpened = true + } + } + + Component.onCompleted: { + parseMessage(root.messageText); + } + } + + linksComponent: Component { + LinksMessageView { + linkUrls: root.linkUrls + container: root + messageStore: root.messageStore + store: root.rootStore + isCurrentUser: root.amISender + } + } + + quickActions: [ + StatusFlatRoundButton { + id: emojiBtn + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: "reaction-b" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Add reaction") + onClicked: { + root.setMessageActive(model.id, true) + // Set parent, X & Y positions for the messageContextMenu + root.messageContextMenu.parent = parent + root.messageContextMenu.setXPosition = function() { return (-Math.abs(parent.width - root.messageContextMenu.emojiContainer.width))} + root.messageContextMenu.setYPosition = function() { return (-root.messageContextMenu.height - 4)} + root.messageClickHandler(false, false, false, null, true, false) + } + }, + StatusFlatRoundButton { + id: replyBtn + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: "reply" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Reply") + onClicked: { + root.showReplyArea(model.id, model.senderId) + if (messageContextMenu.closeParentPopup) { + messageContextMenu.closeParentPopup() + } + } + }, + StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + icon.source: Style.svg("edit-message") + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Edit") + enabled: isText && !root.editModeOn && root.amISender + onClicked: { + root.messageStore.setEditModeOn(root.messageId) + } + }, + StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + icon.name: model.pinned ? "unpin" : "pin" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: model.pinned ? qsTr("Unpin") : qsTr("Pin") + enabled: { + 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); + + } + onClicked: { + if (root.pinnedMessage) { + messageStore.unpinMessage(model.id) + return; + } + + if (d.canPin) { + messageStore.pinMessage(model.id) + 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: model.id + }); + } + }, + StatusFlatRoundButton { + width: d.chatButtonSize + height: d.chatButtonSize + visible: enabled && !root.isInPinnedPopup + icon.name: "delete" + type: StatusFlatRoundButton.Type.Tertiary + tooltip.text: qsTr("Delete") + enabled: { + 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); + } + onClicked: { + if (!localAccountSensitiveSettings.showDeleteMessageWarning) { + messageStore.deleteMessage(model.id) + } + else { + Global.openPopup(deleteMessageConfirmationDialogComponent) + } + } + } + ] + } + } + Component { id: compactMessageComponent CompactMessageView { container: root - store: root.store - message: root.message + store: root.rootStore + message: root.messageText messageStore: root.messageStore usersStore: root.usersStore contactsStore: root.contactsStore @@ -345,16 +749,16 @@ Loader { linkUrls: root.linkUrls isInPinnedPopup: root.isInPinnedPopup pinnedMessage: root.pinnedMessage - canPin: !!messageStore && messageStore.getNumberOfPinnedMessages() < Constants.maxNumberOfPins + canPin: d.canPin transactionParams: root.transactionParams onAddEmoji: { - root.clickMessage(isProfileClick, isSticker, isImage , image, isEmoji, hideEmojiPicker) + root.messageClickHandler(isProfileClick, isSticker, isImage , image, isEmoji, hideEmojiPicker) } onClickMessage: { - root.clickMessage(isProfileClick, isSticker, isImage, image, isEmoji, hideEmojiPicker, isReply, isRightClickOnImage, imageSource) + root.messageClickHandler(isProfileClick, isSticker, isImage, image, isEmoji, hideEmojiPicker, isReply, isRightClickOnImage, imageSource) } onOpenStickerPackPopup: { @@ -365,7 +769,32 @@ Loader { root.showReplyArea(messageId, author) } - onImageClicked: root.imageClicked(image) + onImageClicked: { + console.log("Image clicked:", image) + root.imageClicked(image) + } + } + } + + 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/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..5b72b6fc301 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -52,6 +52,7 @@ QtObject { return Style.current.accountColors[colorIndex] } + // WARNING: Remove. The function moved to StatusQ.Core.Utils function getMessageWithStyle(msg, isCurrentUser, hoveredLink = "") { return `