fix: Create only one instance of `StatusChatInput` (#10928)

* Chat input area preserved properties
* Fix emoji/gif/stickers popups open/close logic
This commit is contained in:
Igor Sirotin 2023-06-07 16:18:29 +03:00 committed by GitHub
parent dc20651a97
commit bc4492b53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 559 additions and 463 deletions

View File

@ -21,6 +21,7 @@ QtObject:
isUntrustworthy: bool
isContact: bool
active: bool
blocked: bool
proc delete*(self: ChatDetails) =
self.QObject.delete
@ -32,7 +33,7 @@ QtObject:
proc setChatDetails*(self: ChatDetails, id: string, `type`: int, belongsToCommunity,
isUsersListAvailable: bool, name, icon: string, color, description,
emoji: string, hasUnreadMessages: bool, notificationsCount: int, muted: bool, position: int,
isUntrustworthy: bool, isContact: bool = false) =
isUntrustworthy: bool, isContact: bool = false, blocked: bool = false) =
self.id = id
self.`type` = `type`
self.belongsToCommunity = belongsToCommunity
@ -49,6 +50,7 @@ QtObject:
self.isUntrustworthy = isUntrustworthy
self.isContact = isContact
self.active = false
self.blocked = blocked
proc getId(self: ChatDetails): string {.slot.} =
return self.id
@ -201,3 +203,14 @@ QtObject:
proc setActive*(self: ChatDetails, value: bool) =
self.active = value
self.activeChanged()
proc blockedChanged(self: ChatDetails) {.signal.}
proc getBlocked(self: ChatDetails): bool {.slot.} =
return self.blocked
QtProperty[bool] blocked:
read = getBlocked
notify = blockedChanged
proc setBlocked*(self: ChatDetails, value: bool) =
self.blocked = value
self.blockedChanged()

View File

@ -142,11 +142,13 @@ proc init*(self: Controller) =
var args = ContactArgs(e)
if (args.contactId == self.chatId):
self.delegate.onMutualContactChanged()
self.delegate.onContactDetailsUpdated(args.contactId)
self.events.on(SIGNAL_CONTACT_UNBLOCKED) do(e: Args):
var args = ContactArgs(e)
if (args.contactId == self.chatId):
self.delegate.onMutualContactChanged()
self.delegate.onContactDetailsUpdated(args.contactId)
self.events.on(SIGNAL_MESSAGE_DELETION) do(e: Args):
let args = MessageDeletedArgs(e)

View File

@ -1,21 +0,0 @@
import NimQml
QtObject:
type
Item* = ref object of QObject
proc setup(self: Item) =
self.QObject.setup
proc delete*(self: Item) =
self.QObject.delete
proc newItem*(): Item =
new(result, delete)
result.setup()
proc id*(self: Item): string {.slot.} =
self.id
QtProperty[string] id:
read = id

View File

@ -1,17 +0,0 @@
import NimQml
import item
QtObject:
type
Model* = ref object of QAbstractListModel
sections: seq[Item]
proc setup(self: Model) =
self.QAbstractListModel.setup
proc delete*(self: Model) =
self.QAbstractListModel.delete
proc newModel*(): Model =
new(result, delete)
result.setup

View File

@ -0,0 +1,58 @@
import NimQml
QtObject:
type
PreservedProperties* = ref object of QObject
text: string
replyMessageId: string
fileUrlsAndSourcesJson: string
proc setup(self: PreservedProperties) =
self.QObject.setup
self.fileUrlsAndSourcesJson = "[]"
proc delete*(self: PreservedProperties) =
self.QObject.delete
proc newPreservedProperties*(): PreservedProperties =
new(result, delete)
result.QObject.setup
proc textChanged*(self: PreservedProperties) {.signal.}
proc setText*(self: PreservedProperties, value: string) {.slot.} =
if self.text == value:
return
self.text = value
self.textChanged()
proc getText*(self: PreservedProperties): string {.slot.} =
result = self.text
QtProperty[string] text:
read = getText
write = setText
notify = textChanged
proc replyMessageIdChanged*(self: PreservedProperties) {.signal.}
proc setReplyMessageId*(self: PreservedProperties, value: string) {.slot.}=
if self.replyMessageId == value:
return
self.replyMessageId = value
self.replyMessageIdChanged()
proc getReplyMessageId*(self: PreservedProperties): string {.slot.} =
result = self.replyMessageId
QtProperty[string] replyMessageId:
read = getReplyMessageId
write = setReplyMessageId
notify = replyMessageIdChanged
proc fileUrlsAndSourcesJsonChanged*(self: PreservedProperties) {.signal.}
proc setFileUrlsAndSourcesJson*(self: PreservedProperties, value: string) {.slot.}=
if self.fileUrlsAndSourcesJson == value:
return
self.fileUrlsAndSourcesJson = value
self.fileUrlsAndSourcesJsonChanged()
proc getFileUrlsAndSourcesJson*(self: PreservedProperties): string {.slot.} =
result = self.fileUrlsAndSourcesJson
QtProperty[string] fileUrlsAndSourcesJson:
read = getFileUrlsAndSourcesJson
write = setFileUrlsAndSourcesJson
notify = fileUrlsAndSourcesJsonChanged

View File

@ -1,32 +1,38 @@
import NimQml
import ./model
import ./io_interface
import ./gif_column_model
import ./preserved_properties
import ../../../../../../app_service/service/gif/dto
QtObject:
type
View* = ref object of QObject
delegate: io_interface.AccessInterface
model: Model
gifColumnAModel: GifColumnModel
gifColumnBModel: GifColumnModel
gifColumnCModel: GifColumnModel
gifLoading: bool
preservedProperties: PreservedProperties
preservedPropertiesVariant: QVariant
proc delete*(self: View) =
self.model.delete
self.QObject.delete
self.gifColumnAModel.delete
self.gifColumnBModel.delete
self.gifColumnCModel.delete
self.preservedProperties.delete
self.preservedPropertiesVariant.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
result.QObject.setup
result.delegate = delegate
result.model = newModel()
result.gifColumnAModel = newGifColumnModel()
result.gifColumnBModel = newGifColumnModel()
result.gifColumnCModel = newGifColumnModel()
result.gifLoading = false
result.preservedProperties = newPreservedProperties()
result.preservedPropertiesVariant = newQVariant(result.preservedProperties)
proc load*(self: View) =
self.delegate.viewDidLoad()
@ -171,3 +177,9 @@ QtObject:
proc isFavorite*(self: View, id: string): bool {.slot.} =
let gifItem = self.findGifDto(id)
return self.delegate.isFavorite(gifItem)
proc getPreservedProperties(self: View): QVariant {.slot.} =
return self.preservedPropertiesVariant
QtProperty[QVariant] preservedProperties:
read = getPreservedProperties

View File

@ -358,6 +358,7 @@ method onContactDetailsUpdated*(self: Module, contactId: string) =
if(self.controller.getMyChatId() == contactId):
self.view.updateChatDetailsNameAndIcon(updatedContact.defaultDisplayName, updatedContact.icon)
self.view.updateTrustStatus(updatedContact.dto.trustStatus == TrustStatus.Untrustworthy)
self.view.updateChatBlocked(updatedContact.dto.blocked)
method onNotificationsUpdated*(self: Module, hasUnreadMessages: bool, notificationCount: int) =
self.view.updateChatDetailsNotifications(hasUnreadMessages, notificationCount)

View File

@ -146,4 +146,7 @@ QtObject:
self.chatDetails.setIsMutualContact(value)
proc downloadMessages*(self: View, filePath: string) {.slot.} =
self.delegate.downloadMessages(filePath)
self.delegate.downloadMessages(filePath)
proc updateChatBlocked*(self: View, blocked: bool) =
self.chatDetails.setBlocked(blocked)

View File

@ -62,7 +62,7 @@ Rectangle {
anchors.bottomMargin: 8
font.pixelSize: 13
font.weight: Font.Medium
color: Theme.palette.white
color: Theme.palette.directColor1
}
}
}

View File

@ -1,6 +1,7 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQml 2.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
@ -18,6 +19,7 @@ import "../controls"
import "../popups"
import "../panels"
import "../../Wallet"
import "../stores"
Item {
id: root
@ -40,7 +42,6 @@ Item {
readonly property var contactDetails: rootStore ? rootStore.oneToOneChatContact : null
readonly property bool isUserAdded: root.contactDetails && root.contactDetails.isAdded
signal openAppSearch()
signal openStickerPackPopup(string stickerPackId)
function requestAddressForTransaction(address, amount, tokenAddress, tokenDecimals = 18) {
@ -98,6 +99,84 @@ Item {
root.createChatPropertiesStore.resetProperties()
}
QtObject {
id: d
readonly property var activeChatContentModule: d.getChatContentModule(root.activeChatId)
readonly property UsersStore activeUsersStore: UsersStore {
usersModule: !!d.activeChatContentModule ? d.activeChatContentModule.usersModule : null
}
readonly property MessageStore activeMessagesStore: MessageStore {
messageModule: d.activeChatContentModule ? d.activeChatContentModule.messagesModule : null
chatSectionModule: root.rootStore.chatCommunitySectionModule
}
function getChatContentModule(chatId) {
root.parentModule.prepareChatContentModuleForChatId(chatId)
return root.parentModule.getChatContentModule()
}
function showReplyArea(messageId) {
const obj = d.activeMessagesStore.getMessageByIdAsJson(messageId)
if (!obj)
return
chatInput.showReplyArea(messageId,
obj.senderDisplayName,
obj.messageText,
obj.contentType,
obj.messageImage,
obj.albumMessageImages,
obj.albumImagesCount,
obj.sticker)
}
function restoreInputReply() {
const replyMessageId = d.activeChatContentModule.inputAreaModule.preservedProperties.replyMessageId
if (replyMessageId)
d.showReplyArea(replyMessageId)
else
chatInput.resetReplyArea()
}
function restoreInputAttachments() {
const filesJson = d.activeChatContentModule.inputAreaModule.preservedProperties.fileUrlsAndSourcesJson
let filesList = []
if (filesJson) {
try {
filesList = JSON.parse(filesJson)
} catch(e) {
console.error("failed to parse preserved fileUrlsAndSources")
}
}
chatInput.resetImageArea()
chatInput.validateImagesAndShowImageArea(filesList)
}
function restoreInputState() {
if (!d.activeChatContentModule) {
chatInput.textInput.text = ""
chatInput.resetReplyArea()
chatInput.resetImageArea()
return
}
// Restore message text
chatInput.textInput.text = d.activeChatContentModule.inputAreaModule.preservedProperties.text
chatInput.textInput.cursorPosition = chatInput.textInput.length
d.restoreInputReply()
d.restoreInputAttachments()
}
onActiveChatContentModuleChanged: {
// Call later to make sure activeUsersStore and activeMessagesStore bindings are updated
Qt.callLater(d.restoreInputState)
}
}
EmptyChatPanel {
anchors.fill: parent
visible: root.activeChatId === "" || root.chatsCount == 0
@ -107,46 +186,150 @@ Item {
// This is kind of a solution for applying backend refactored changes with the minimal qml changes.
// The best would be if we made qml to follow the struct we have on the backend side.
Repeater {
id: chatRepeater
model: parentModule && parentModule.model
ChatContentView {
width: parent.width
height: parent.height
visible: !root.rootStore.openCreateChat && isActiveChannel
chatId: model.itemId
chatType: model.type
chatMessagesLoader.active: model.loaderActive
rootStore: root.rootStore
contactsStore: root.contactsStore
ColumnLayout {
anchors.fill: parent
spacing: 0
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Repeater {
id: chatRepeater
model: parentModule && parentModule.model
ChatContentView {
width: parent.width
height: parent.height
visible: !root.rootStore.openCreateChat && model.active
chatId: model.itemId
chatType: model.type
chatMessagesLoader.active: model.loaderActive
rootStore: root.rootStore
contactsStore: root.contactsStore
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
stickersLoaded: root.stickersLoaded
isBlocked: model.blocked
onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId)
}
onShowReplyArea: (messageId) => {
d.showReplyArea(messageId)
}
onForceInputFocus: {
chatInput.forceInputActiveFocus()
}
Component.onCompleted: {
chatContentModule = d.getChatContentModule(model.itemId)
chatSectionModule = root.parentModule
root.checkForCreateChatOptions(model.itemId)
}
}
}
}
StatusChatInput {
id: chatInput
Layout.fillWidth: true
Layout.margins: Style.current.smallPadding
enabled: root.rootStore.sectionDetails.joined && !root.rootStore.sectionDetails.amIBanned &&
root.rootStore.isUserAllowedToSendMessage
store: root.rootStore
usersStore: d.usersStore
textInput.placeholderText: {
if (d.activeChatContentModule.chatDetails.blocked)
return qsTr("This user has been blocked.")
if (!root.rootStore.sectionDetails.joined || root.rootStore.sectionDetails.amIBanned)
return qsTr("You need to join this community to send messages")
return root.rootStore.chatInputPlaceHolderText
}
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
sendTransactionNoEnsModal: cmpSendTransactionNoEns
receiveTransactionModal: cmpReceiveTransaction
sendTransactionWithEnsModal: cmpSendTransactionWithEns
stickersLoaded: root.stickersLoaded
isBlocked: model.blocked
isActiveChannel: model.active
onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId)
isContactBlocked: d.activeChatContentModule.chatDetails.blocked
chatType: root.activeChatType
suggestions.suggestionFilter.addSystemSuggestions: chatType === Constants.chatType.communityChat
textInput.onTextChanged: {
d.activeChatContentModule.inputAreaModule.preservedProperties.text = textInput.text
}
onOpenAppSearch: {
root.openAppSearch();
onReplyMessageIdChanged: {
d.activeChatContentModule.inputAreaModule.preservedProperties.replyMessageId = replyMessageId
}
Component.onCompleted: {
parentModule.prepareChatContentModuleForChatId(model.itemId)
chatContentModule = parentModule.getChatContentModule()
chatSectionModule = root.parentModule
root.checkForCreateChatOptions(model.itemId)
onFileUrlsAndSourcesChanged: {
d.activeChatContentModule.inputAreaModule.preservedProperties.fileUrlsAndSourcesJson = JSON.stringify(chatInput.fileUrlsAndSources)
}
onSendTransactionCommandButtonClicked: {
if (!d.activeChatContentModule) {
console.warn("error on sending transaction command - chat content module is not set")
return
}
if (Utils.isEnsVerified(d.activeChatContentModule.getMyChatId())) {
Global.openPopup(cmpSendTransactionWithEns)
} else {
Global.openPopup(cmpSendTransactionNoEns)
}
}
onReceiveTransactionCommandButtonClicked: {
Global.openPopup(cmpReceiveTransaction)
}
onStickerSelected: {
root.rootStore.sendSticker(d.activeChatContentModule.getMyChatId(),
hashId,
chatInput.isReply ? chatInput.replyMessageId : "",
packId,
url)
}
onSendMessage: {
if (!d.activeChatContentModule) {
console.debug("error on sending message - chat content module is not set")
return
}
if (root.rootStore.sendMessage(d.activeChatContentModule.getMyChatId(),
event,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrlsAndSources
))
{
Global.playSendMessageSound()
chatInput.textInput.clear();
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
}
onUnblockChat: {
d.activeChatContentModule.unblockChat()
}
onKeyUpPress: {
d.activeMessagesStore.setEditModeOnLastMessage(root.rootStore.userProfileInst.pubKey)
}
}
}
Component {
id: cmpSendTransactionNoEns
ChatCommandModal {
id: sendTransactionNoEns
store: root.rootStore
contactsStore: root.contactsStore
onClosed: {
@ -178,7 +361,6 @@ Item {
Component {
id: cmpReceiveTransaction
ChatCommandModal {
id: receiveTransaction
store: root.rootStore
contactsStore: root.contactsStore
onClosed: {
@ -209,7 +391,6 @@ Item {
Component {
id: cmpSendTransactionWithEns
SendModal {
id: sendTransactionWithEns
onClosed: {
destroy()
}

View File

@ -24,16 +24,12 @@ import "../stores"
ColumnLayout {
id: root
objectName: "chatContentViewColumn"
spacing: 0
// Important:
// Each chat/channel has its own ChatContentModule
// Important: each chat/channel has its own ChatContentModule
property var chatContentModule
property var chatSectionModule
property var rootStore
property var contactsStore
property bool isActiveChannel: false
property string chatId
property int chatType: Constants.chatType.unknown
@ -43,22 +39,27 @@ ColumnLayout {
property var stickersPopup
property UsersStore usersStore: UsersStore {}
onChatContentModuleChanged: if (!!chatContentModule) {
root.usersStore.usersModule = root.chatContentModule.usersModule
}
signal openAppSearch()
signal openStickerPackPopup(string stickerPackId)
property Component sendTransactionNoEnsModal
property Component receiveTransactionModal
property Component sendTransactionWithEnsModal
property bool isBlocked: false
property bool isUserAllowedToSendMessage: root.rootStore.isUserAllowedToSendMessage
property string chatInputPlaceholder: root.rootStore.chatInputPlaceHolderText
property bool stickersLoaded: false
readonly property var messageStore: MessageStore {
messageModule: chatContentModule ? chatContentModule.messagesModule : null
chatSectionModule: root.rootStore.chatCommunitySectionModule
}
signal showReplyArea(messageId: string)
signal forceInputFocus()
objectName: "chatContentViewColumn"
spacing: 0
onChatContentModuleChanged: if (!!chatContentModule) {
root.usersStore.usersModule = root.chatContentModule.usersModule
}
Loader {
Layout.fillWidth: true
active: root.isBlocked
@ -69,184 +70,33 @@ ColumnLayout {
}
}
readonly property var messageStore: MessageStore {
messageModule: chatContentModule ? chatContentModule.messagesModule : null
chatSectionModule: root.rootStore.chatCommunitySectionModule
}
QtObject {
id: d
readonly property string blockedText: qsTr("This user has been blocked.")
function showReplyArea(messageId) {
let obj = messageStore.getMessageByIdAsJson(messageId)
if (!obj) {
return
}
if (inputAreaLoader.item) {
inputAreaLoader.item.chatInput.showReplyArea(messageId, obj.senderDisplayName, obj.messageText, obj.contentType, obj.messageImage, obj.albumMessageImages, obj.albumImagesCount, obj.sticker)
}
}
}
ColumnLayout {
Loader {
id: chatMessagesLoader
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Loader {
id: chatMessagesLoader
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: ChatMessagesView {
chatContentModule: root.chatContentModule
rootStore: root.rootStore
contactsStore: root.contactsStore
messageStore: root.messageStore
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
usersStore: root.usersStore
stickersLoaded: root.stickersLoaded
chatId: root.chatId
isOneToOne: root.chatType === Constants.chatType.oneToOne
isContactBlocked: root.isBlocked
isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage
channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
isActiveChannel: root.isActiveChannel
onShowReplyArea: (messageId, senderId) => {
d.showReplyArea(messageId)
}
onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId);
}
onEditModeChanged: {
if (!editModeOn && inputAreaLoader.item)
inputAreaLoader.item.chatInput.forceInputActiveFocus()
}
sourceComponent: ChatMessagesView {
chatContentModule: root.chatContentModule
rootStore: root.rootStore
contactsStore: root.contactsStore
messageStore: root.messageStore
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
usersStore: root.usersStore
stickersLoaded: root.stickersLoaded
chatId: root.chatId
isOneToOne: root.chatType === Constants.chatType.oneToOne
isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage
channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
onShowReplyArea: (messageId, senderId) => {
root.showReplyArea(messageId)
}
}
Loader {
id: inputAreaLoader
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
active: root.isActiveChannel
asynchronous: true
property string preservedText
Binding on preservedText {
when: inputAreaLoader.item != null
value: inputAreaLoader.item ? inputAreaLoader.item.chatInput.textInput.text : inputAreaLoader.preservedText
restoreMode: Binding.RestoreNone
delayed: true
onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId);
}
// FIXME: `StatusChatInput` is way too heavy
// see: https://github.com/status-im/status-desktop/pull/10343#issuecomment-1515103756
sourceComponent: Item {
id: inputArea
implicitHeight: chatInput.implicitHeight
+ chatInput.anchors.topMargin
+ chatInput.anchors.bottomMargin
readonly property alias chatInput: chatInput
StatusChatInput {
id: chatInput
anchors.fill: parent
anchors.margins: Style.current.smallPadding
// We enable the component if the contact is blocked, because if we disable it, the `Unban` button
// becomes disabled. All the local components inside already disable themselves when blocked
enabled: root.isBlocked ||
(root.rootStore.sectionDetails.joined && !root.rootStore.sectionDetails.amIBanned &&
root.isUserAllowedToSendMessage)
store: root.rootStore
usersStore: root.usersStore
textInput.text: inputAreaLoader.preservedText
textInput.placeholderText: root.isBlocked ? d.blockedText : root.chatInputPlaceholder
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
isContactBlocked: root.isBlocked
isActiveChannel: root.isActiveChannel
anchors.bottom: parent.bottom
chatType: root.chatType
suggestions.suggestionFilter.addSystemSuggestions: chatType === Constants.chatType.communityChat
Binding on chatInputPlaceholder {
when: root.isBlocked
value: d.blockedText
}
Binding on chatInputPlaceholder {
when: !root.rootStore.sectionDetails.joined || root.rootStore.sectionDetails.amIBanned
value: qsTr("You need to join this community to send messages")
}
onSendTransactionCommandButtonClicked: {
if(!chatContentModule) {
console.debug("error on sending transaction command - chat content module is not set")
return
}
if (Utils.isEnsVerified(chatContentModule.getMyChatId())) {
Global.openPopup(root.sendTransactionWithEnsModal)
} else {
Global.openPopup(root.sendTransactionNoEnsModal)
}
}
onReceiveTransactionCommandButtonClicked: {
Global.openPopup(root.receiveTransactionModal)
}
onStickerSelected: {
root.rootStore.sendSticker(chatContentModule.getMyChatId(),
hashId,
chatInput.isReply ? chatInput.replyMessageId : "",
packId,
url)
}
onSendMessage: {
if (!chatContentModule) {
console.debug("error on sending message - chat content module is not set")
return
}
if(root.rootStore.sendMessage(chatContentModule.getMyChatId(),
event,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrlsAndSources
))
{
Global.playSendMessageSound()
chatInput.textInput.clear();
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
}
onUnblockChat: {
chatContentModule.unblockChat()
}
onKeyUpPress: messageStore.setEditModeOnLastMessage(root.rootStore.userProfileInst.pubKey)
Component.onCompleted: {
Qt.callLater(() => {
forceInputActiveFocus()
textInput.cursorPosition = textInput.length
})
}
}
onEditModeChanged: {
if (!editModeOn)
root.forceInputFocus()
}
}
}

View File

@ -251,7 +251,6 @@ Item {
chatLogView: ListView.view
chatContentModule: root.chatContentModule
isActiveChannel: root.isActiveChannel
isChatBlocked: root.isChatBlocked
chatId: root.chatId

View File

@ -89,9 +89,6 @@ StatusSectionLayout {
onOpenStickerPackPopup: {
Global.openPopup(statusStickerPackClickPopup, {packId: stickerPackId, store: root.stickersPopup.store} )
}
onOpenAppSearch: {
root.openAppSearch();
}
}
showRightPanel: {

View File

@ -433,7 +433,6 @@ Item {
enabled: communityData.amISectionAdmin
acceptedButtons: Qt.RightButton
onTapped: {
console.log("<<< tapped")
adminPopupMenu.showInviteButton = true
adminPopupMenu.x = eventPoint.position.x + 4
adminPopupMenu.y = eventPoint.position.y + 4

View File

@ -36,19 +36,16 @@ Rectangle {
property var emojiPopup: null
property var stickersPopup: null
// Use this to only enable the Connections only when this Input opens the Emoji popup
property bool emojiPopupOpened: false
property bool stickersPopupOpened: false
property bool closeGifPopupAfterSelection: true
property bool emojiEvent: false
property bool isColonPressed: false
property bool isReply: false
property string replyMessageId: replyArea.messageId
readonly property string replyMessageId: replyArea.messageId
property bool isImage: false
property bool isEdit: false
property bool isContactBlocked: false
property bool isActiveChannel: false
property int messageLimit: 2000
property int messageLimitVisible: 200
@ -131,36 +128,52 @@ Rectangle {
property int leftOfMentionIndex: -1
property int rightOfMentionIndex: -1
readonly property int nbEmojisInClipboard: StatusQUtils.Emoji.nbEmojis(QClipboardProxy.html)
property bool emojiPopupOpened: false
property bool stickersPopupOpened: false
// common popups are emoji, jif and stickers
// Put controlWidth as argument with default value for binding
function getCommonPopupRelativePosition(popup, popupParent, controlWidth = control.width) {
const controlX = controlWidth - emojiPopup.width - Style.current.halfPadding
const controlY = -emojiPopup.height
return popupParent.mapFromItem(control, controlX, controlY)
}
readonly property point emojiPopupPosition: getCommonPopupRelativePosition(emojiPopup, emojiBtn)
readonly property point stickersPopupPosition: getCommonPopupRelativePosition(stickersPopup, stickersBtn)
readonly property StateGroup emojiPopupTakeover: StateGroup {
states: State {
when: control.emojiPopupOpened
when: d.emojiPopupOpened
PropertyChanges {
target: emojiPopup
parent: control
parent: emojiBtn
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
x: control.width - emojiPopup.width - Style.current.halfPadding
y: -emojiPopup.height
x: d.emojiPopupPosition.x
y: d.emojiPopupPosition.y
}
}
}
readonly property StateGroup stickersPopupTakeover: StateGroup {
states: State {
when: control.stickersPopupOpened
when: d.stickersPopupOpened
PropertyChanges {
target: stickersPopup
parent: control
parent: stickersBtn
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
x: control.width - stickersPopup.width - Style.current.halfPadding
y: -stickersPopup.height
x: d.stickersPopupPosition.x
y: d.stickersPopupPosition.y
}
}
}
property bool chatCommandsPopupOpen: false
property Menu textFormatMenu: null
function copyMentions(start, end) {
copiedMentionsPos = []
@ -268,30 +281,8 @@ Rectangle {
messageInputField.insert(start, text.replace(/\n/g, "<br/>"));
}
function togglePopup(popup, btn) {
if (popup !== control.stickersPopup) {
control.stickersPopup.close()
}
if (popup !== gifPopup) {
gifPopup.close()
}
if (popup !== emojiPopup) {
emojiPopup.close()
}
if (popup.opened) {
popup.close()
btn.highlighted = false
} else {
popup.open()
btn.highlighted = true
}
}
Connections {
enabled: control.emojiPopupOpened
enabled: d.emojiPopupOpened
target: emojiPopup
function onEmojiSelected(text: string, atCursor: bool) {
@ -300,13 +291,12 @@ Rectangle {
messageInputField.forceActiveFocus();
}
function onClosed() {
emojiBtn.highlighted = false
control.emojiPopupOpened = false
d.emojiPopupOpened = false
}
}
Connections {
enabled: control.stickersPopupOpened
enabled: d.stickersPopupOpened
target: control.stickersPopup
function onStickerSelected(hashId: string, packId: string, url: string ) {
@ -315,15 +305,14 @@ Rectangle {
messageInputField.forceActiveFocus();
}
function onClosed() {
stickersBtn.highlighted = false
control.stickersPopupOpened = false
d.stickersPopupOpened = false
}
}
property var mentionsPos: []
function isUploadFilePressed(event) {
return (event.key === Qt.Key_U) && (event.modifiers & Qt.ControlModifier) && imageBtn.visible && !imageDialog.visible
return (event.key === Qt.Key_U) && (event.modifiers & Qt.ControlModifier) && imageBtn.visible && !imageBtn.highlighted
}
function checkTextInsert() {
@ -354,7 +343,7 @@ Rectangle {
}
if (event) {
event.accepted = true
messageTooLongDialog.open()
console.error("Attempting to send a message exceeding length limit")
}
} else if (event.key === Qt.Key_Escape && control.isReply) {
control.isReply = false
@ -664,8 +653,8 @@ Rectangle {
if ((event.modifiers & Qt.ControlModifier) || (event.modifiers & Qt.MetaModifier)) // these are likely shortcuts with no meaningful text
return
if (event.key === Qt.Key_Backspace && textFormatMenu.opened) {
textFormatMenu.close()
if (event.key === Qt.Key_Backspace && d.textFormatMenu.opened) {
d.textFormatMenu.close()
}
// the text doesn't get registered to the textarea fast enough
// we can only get it in the `released` event
@ -883,19 +872,27 @@ Rectangle {
return true;
}
function hideExtendedArea() {
function resetImageArea() {
isImage = false;
isReply = false;
control.fileUrlsAndSources = []
imageArea.imageSource = [];
replyArea.userName = ""
replyArea.message = ""
for (let i=0; i<validators.children.length; i++) {
const validator = validators.children[i]
validator.images = []
}
}
function resetReplyArea() {
isReply = false;
replyArea.userName = ""
replyArea.message = ""
}
function hideExtendedArea() {
resetImageArea()
resetReplyArea()
}
function validateImages(imagePaths) {
if (!imagePaths || !imagePaths.length) {
return []
@ -912,7 +909,7 @@ Rectangle {
}
function showImageArea(imagePathsOrData) {
isImage = true;
isImage = imagePathsOrData.length > 0
imageArea.imageSource = imagePathsOrData
control.fileUrlsAndSources = imageArea.imageSource
}
@ -921,12 +918,8 @@ Rectangle {
// Returns true if the images were valid and added
function validateImagesAndShowImageArea(imagePaths) {
const validImages = validateImages(imagePaths)
if (validImages.length > 0) {
showImageArea(validImages)
return true
}
return false
showImageArea(validImages)
return isImage
}
function showReplyArea(messageId, userName, message, contentType, image, album, albumCount, sticker) {
@ -947,7 +940,6 @@ Rectangle {
}
Connections {
enabled: control.isActiveChannel
target: Global.dragArea
ignoreUnknownSignals: true
function onDroppedOnValidScreen(drop) {
@ -963,30 +955,25 @@ Rectangle {
messageInputField.forceActiveFocus();
}
FileDialog {
id: imageDialog
title: qsTr("Please choose an image")
folder: shortcuts.pictures
selectMultiple: true
nameFilters: [
qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))
]
onAccepted: {
imageBtn.highlighted = false
validateImagesAndShowImageArea(imageDialog.fileUrls)
messageInputField.forceActiveFocus();
}
onRejected: {
imageBtn.highlighted = false
}
}
Component {
id: imageDialogComponent
MessageDialog {
id: messageTooLongDialog
title: qsTr("Your message is too long.")
icon: StandardIcon.Critical
text: qsTr("Please make your message shorter. We have set the limit to 2000 characters to be courteous of others.")
standardButtons: StandardButton.Ok
FileDialog {
title: qsTr("Please choose an image")
folder: shortcuts.pictures
selectMultiple: true
nameFilters: [
qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))
]
onAccepted: {
imageBtn.highlighted = false
validateImagesAndShowImageArea(fileUrls)
messageInputField.forceActiveFocus()
}
onRejected: {
imageBtn.highlighted = false
}
}
}
StatusEmojiSuggestionPopup {
@ -1026,51 +1013,59 @@ Rectangle {
}
}
Component {
id: chatCommandsPopup
id: chatCommandsPopupComponent
ChatCommandsPopup {
x: (parent.width - control.width - Style.current.halfPadding)
y: 716
id: chatCommandsPopup
x: 8
y: -height
onSendTransactionCommandButtonClicked: {
control.sendTransactionCommandButtonClicked();
chatCommandsPopup.close();
control.sendTransactionCommandButtonClicked()
close()
}
onReceiveTransactionCommandButtonClicked: {
control.receiveTransactionCommandButtonClicked();
chatCommandsPopup.close();
control.receiveTransactionCommandButtonClicked()
close()
}
onClosed: {
chatCommandsBtnLoader.highlighted = false;
destroy();
chatCommandsBtn.highlighted = false
destroy()
}
onOpened: {
chatCommandsBtn.highlighted = true
}
Component.onDestruction: {
if (d.chatCommandsPopupOpen)
d.chatCommandsPopupOpen = false;
}
onOpened: {
chatCommandsBtnLoader.highlighted = true;
}
}
}
StatusGifPopup {
id: gifPopup
width: 360
height: 440
x: control.width - width - Style.current.halfPadding
y: -height
gifSelected: function (event, url) {
messageInputField.text += "\n" + url
control.sendMessage(event)
control.isReply = false
gifBtn.highlighted = false
messageInputField.forceActiveFocus()
if (control.closeGifPopupAfterSelection)
gifPopup.close()
}
onClosed: {
gifBtn.highlighted = false
Component {
id: gifPopupComponent
StatusGifPopup {
readonly property point relativePosition: d.getCommonPopupRelativePosition(this, parent)
width: 360
height: 440
x: relativePosition.x
y: relativePosition.y
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
gifSelected: function (event, url) {
messageInputField.text += "\n" + url
control.sendMessage(event)
control.isReply = false
messageInputField.forceActiveFocus()
if (control.closeGifPopupAfterSelection)
close()
}
onClosed: {
gifBtn.popup = null
destroy()
}
}
}
@ -1091,12 +1086,12 @@ Rectangle {
Layout.bottomMargin: 4
icon.name: "chat-commands"
type: StatusQ.StatusFlatRoundButton.Type.Tertiary
visible: RootStore.isWalletEnabled && !isEdit && control.chatType === Constants.chatType.oneToOne
enabled: !control.isContactBlocked
onClicked: {
d.chatCommandsPopupOpen ? Global.closePopup() : Global.openPopup(chatCommandsPopup);
d.chatCommandsPopupOpen = !d.chatCommandsPopupOpen;
}
visible: RootStore.isWalletEnabled && !isEdit && control.chatType === Constants.chatType.oneToOne
}
}
@ -1112,7 +1107,8 @@ Rectangle {
enabled: !control.isContactBlocked
onClicked: {
highlighted = true
imageDialog.open()
const popup = imageDialogComponent.createObject(control)
popup.open()
}
}
@ -1123,7 +1119,6 @@ Rectangle {
Layout.fillWidth: true
implicitHeight: inputLayout.implicitHeight + inputLayout.anchors.topMargin + inputLayout.anchors.bottomMargin
implicitWidth: inputLayout.implicitWidth + inputLayout.anchors.leftMargin + inputLayout.anchors.rightMargin
@ -1131,60 +1126,67 @@ Rectangle {
color: isEdit ? Theme.palette.statusChatInput.secondaryBackgroundColor : Style.current.inputBackground
radius: 20
StatusTextFormatMenu {
id: textFormatMenu
Component {
id: textFormatMenuComponent
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)
}
StatusChatInputTextFormationAction {
wrapper: "> "
icon.name: "quote"
text: qsTr("Quote")
checked: messageInputField.selectedText && isSelectedLinePrefixedBy(messageInputField.selectionStart, wrapper)
StatusTextFormatMenu {
id: textFormatMenu
onActionTriggered: checked
? unprefixSelectedLine(wrapper)
: prefixSelectedLine(wrapper)
}
onClosed: {
messageInputField.deselect();
onClosed: {
messageInputField.deselect()
destroy()
}
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)
}
StatusChatInputTextFormationAction {
wrapper: "> "
icon.name: "quote"
text: qsTr("Quote")
checked: messageInputField.selectedText && isSelectedLinePrefixedBy(messageInputField.selectionStart, wrapper)
onActionTriggered: checked
? unprefixSelectedLine(wrapper)
: prefixSelectedLine(wrapper)
}
}
}
ColumnLayout {
id: validators
anchors.bottom: control.imageErrorMessageLocation === StatusChatInput.ImageErrorMessageLocation.Top ? parent.top : undefined
@ -1244,8 +1246,7 @@ Rectangle {
if (control.fileUrlsAndSources.length > index && control.fileUrlsAndSources[index]) {
control.fileUrlsAndSources.splice(index, 1)
}
isImage = control.fileUrlsAndSources.length > 0
validateImages(control.fileUrlsAndSources)
showImageArea(control.fileUrlsAndSources)
}
}
@ -1359,12 +1360,14 @@ Rectangle {
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.y - textFormatMenu.height - 5)
messageInputField.forceActiveFocus();
const x = now < messageInputField.lastClick + 500
? event.x
: (messageInputField.cursorRectangle.x + event.x) / 2
let menu = Global.openMenu(textFormatMenuComponent, messageInput, {})
menu.x = x - menu.width / 2
menu.y = messageInputField.y - menu.height - 5
d.textFormatMenu = menu
messageInputField.forceActiveFocus()
}
lastClick = now
}
@ -1466,15 +1469,22 @@ Rectangle {
: Theme.palette.baseColor1
type: StatusQ.StatusFlatRoundButton.Type.Tertiary
color: "transparent"
highlighted: d.emojiPopupOpened
onClicked: {
control.emojiPopupOpened = true
togglePopup(emojiPopup, emojiBtn)
if (d.emojiPopupOpened) {
emojiPopup.close()
return
}
d.emojiPopupOpened = true
emojiPopup.open()
}
}
StatusQ.StatusFlatRoundButton {
id: gifBtn
property Popup popup
objectName: "gifPopupButton"
implicitHeight: 32
implicitWidth: 32
@ -1484,7 +1494,15 @@ Rectangle {
: Theme.palette.baseColor1
type: StatusQ.StatusFlatRoundButton.Type.Tertiary
color: "transparent"
onClicked: togglePopup(gifPopup, gifBtn)
highlighted: popup && popup.opened
onClicked: {
if (popup) {
popup.close()
return
}
popup = gifPopupComponent.createObject(gifBtn)
popup.open()
}
}
StatusQ.StatusFlatRoundButton {
@ -1499,10 +1517,14 @@ Rectangle {
type: StatusQ.StatusFlatRoundButton.Type.Tertiary
visible: !isEdit && emojiBtn.visible
color: "transparent"
highlighted: d.stickersPopupOpened
onClicked: {
control.stickersPopupOpened = true
togglePopup(control.stickersPopup, stickersBtn)
if (d.stickersPopupOpened) {
control.stickersPopup.close()
return
}
d.stickersPopupOpened = true
control.stickersPopup.open()
}
}
}
@ -1512,8 +1534,7 @@ Rectangle {
}
StatusQ.StatusButton {
id: unblockBtn
Layout.alignment: Qt.AlignBottom
Layout.fillHeight: true
Layout.bottomMargin: 4
visible: control.isContactBlocked
text: qsTr("Unblock")

View File

@ -86,8 +86,6 @@ Row {
}
onClicked: {
imageArea.imageRemoved(index)
const tmp = imageArea.imageSource.filter((url, idx) => idx !== index)
rptImages.model = tmp
}
MouseArea {
id: buttonMouseArea

View File

@ -43,7 +43,6 @@ Popup {
modal: false
width: 360
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
background: Rectangle {
radius: Style.current.radius

View File

@ -8,12 +8,14 @@ import shared.panels 1.0
Item {
id: root
property bool selected: false
property bool useIconInsteadOfImage: false
property url source: Style.svg("history")
signal clicked
height: 24
width: 24
implicitHeight: 24
implicitWidth: 24
RoundedImage {
visible: !useIconInsteadOfImage
@ -25,6 +27,7 @@ Item {
root.clicked()
}
}
RoundedIcon {
id: iconIcon
visible: useIconInsteadOfImage

View File

@ -280,13 +280,12 @@ Popup {
Layout.fillWidth: true
Layout.fillHeight: true
leftPadding: 0
rightPadding: 0
contentWidth: availableWidth
padding: 0
contentHeight: availableHeight
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
RowLayout {
width: scrollView.availableWidth
height: scrollView.availableHeight
spacing: footerContent.spacing
Repeater {
@ -298,8 +297,7 @@ Popup {
delegate: StatusStickerPackIconWithIndicator {
id: packIconWithIndicator
visible: installed
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
selected: stickerPackListView.selectedPackId === packId
source: thumbnail
onClicked: {
@ -314,6 +312,7 @@ Popup {
model: d.stickerPacksLoading ? 7 : 0
delegate: Rectangle {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 24
Layout.preferredHeight: 24
radius: width / 2

View File

@ -25,7 +25,6 @@ Loader {
property var chatContentModule
property string channelEmoji
property bool isActiveChannel: false
property var chatLogView
property var emojiPopup