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 isUntrustworthy: bool
isContact: bool isContact: bool
active: bool active: bool
blocked: bool
proc delete*(self: ChatDetails) = proc delete*(self: ChatDetails) =
self.QObject.delete self.QObject.delete
@ -32,7 +33,7 @@ QtObject:
proc setChatDetails*(self: ChatDetails, id: string, `type`: int, belongsToCommunity, proc setChatDetails*(self: ChatDetails, id: string, `type`: int, belongsToCommunity,
isUsersListAvailable: bool, name, icon: string, color, description, isUsersListAvailable: bool, name, icon: string, color, description,
emoji: string, hasUnreadMessages: bool, notificationsCount: int, muted: bool, position: int, 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.id = id
self.`type` = `type` self.`type` = `type`
self.belongsToCommunity = belongsToCommunity self.belongsToCommunity = belongsToCommunity
@ -49,6 +50,7 @@ QtObject:
self.isUntrustworthy = isUntrustworthy self.isUntrustworthy = isUntrustworthy
self.isContact = isContact self.isContact = isContact
self.active = false self.active = false
self.blocked = blocked
proc getId(self: ChatDetails): string {.slot.} = proc getId(self: ChatDetails): string {.slot.} =
return self.id return self.id
@ -201,3 +203,14 @@ QtObject:
proc setActive*(self: ChatDetails, value: bool) = proc setActive*(self: ChatDetails, value: bool) =
self.active = value self.active = value
self.activeChanged() 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) var args = ContactArgs(e)
if (args.contactId == self.chatId): if (args.contactId == self.chatId):
self.delegate.onMutualContactChanged() self.delegate.onMutualContactChanged()
self.delegate.onContactDetailsUpdated(args.contactId)
self.events.on(SIGNAL_CONTACT_UNBLOCKED) do(e: Args): self.events.on(SIGNAL_CONTACT_UNBLOCKED) do(e: Args):
var args = ContactArgs(e) var args = ContactArgs(e)
if (args.contactId == self.chatId): if (args.contactId == self.chatId):
self.delegate.onMutualContactChanged() self.delegate.onMutualContactChanged()
self.delegate.onContactDetailsUpdated(args.contactId)
self.events.on(SIGNAL_MESSAGE_DELETION) do(e: Args): self.events.on(SIGNAL_MESSAGE_DELETION) do(e: Args):
let args = MessageDeletedArgs(e) 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 NimQml
import ./model
import ./io_interface import ./io_interface
import ./gif_column_model import ./gif_column_model
import ./preserved_properties
import ../../../../../../app_service/service/gif/dto import ../../../../../../app_service/service/gif/dto
QtObject: QtObject:
type type
View* = ref object of QObject View* = ref object of QObject
delegate: io_interface.AccessInterface delegate: io_interface.AccessInterface
model: Model
gifColumnAModel: GifColumnModel gifColumnAModel: GifColumnModel
gifColumnBModel: GifColumnModel gifColumnBModel: GifColumnModel
gifColumnCModel: GifColumnModel gifColumnCModel: GifColumnModel
gifLoading: bool gifLoading: bool
preservedProperties: PreservedProperties
preservedPropertiesVariant: QVariant
proc delete*(self: View) = proc delete*(self: View) =
self.model.delete
self.QObject.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 = proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete) new(result, delete)
result.QObject.setup result.QObject.setup
result.delegate = delegate result.delegate = delegate
result.model = newModel()
result.gifColumnAModel = newGifColumnModel() result.gifColumnAModel = newGifColumnModel()
result.gifColumnBModel = newGifColumnModel() result.gifColumnBModel = newGifColumnModel()
result.gifColumnCModel = newGifColumnModel() result.gifColumnCModel = newGifColumnModel()
result.gifLoading = false result.gifLoading = false
result.preservedProperties = newPreservedProperties()
result.preservedPropertiesVariant = newQVariant(result.preservedProperties)
proc load*(self: View) = proc load*(self: View) =
self.delegate.viewDidLoad() self.delegate.viewDidLoad()
@ -171,3 +177,9 @@ QtObject:
proc isFavorite*(self: View, id: string): bool {.slot.} = proc isFavorite*(self: View, id: string): bool {.slot.} =
let gifItem = self.findGifDto(id) let gifItem = self.findGifDto(id)
return self.delegate.isFavorite(gifItem) 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): if(self.controller.getMyChatId() == contactId):
self.view.updateChatDetailsNameAndIcon(updatedContact.defaultDisplayName, updatedContact.icon) self.view.updateChatDetailsNameAndIcon(updatedContact.defaultDisplayName, updatedContact.icon)
self.view.updateTrustStatus(updatedContact.dto.trustStatus == TrustStatus.Untrustworthy) self.view.updateTrustStatus(updatedContact.dto.trustStatus == TrustStatus.Untrustworthy)
self.view.updateChatBlocked(updatedContact.dto.blocked)
method onNotificationsUpdated*(self: Module, hasUnreadMessages: bool, notificationCount: int) = method onNotificationsUpdated*(self: Module, hasUnreadMessages: bool, notificationCount: int) =
self.view.updateChatDetailsNotifications(hasUnreadMessages, notificationCount) self.view.updateChatDetailsNotifications(hasUnreadMessages, notificationCount)

View File

@ -147,3 +147,6 @@ QtObject:
proc downloadMessages*(self: View, filePath: string) {.slot.} = 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 anchors.bottomMargin: 8
font.pixelSize: 13 font.pixelSize: 13
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.palette.white color: Theme.palette.directColor1
} }
} }
} }

View File

@ -1,6 +1,7 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Controls 2.14 import QtQuick.Controls 2.15
import QtQml 2.14 import QtQuick.Layouts 1.15
import QtQml 2.15
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
@ -18,6 +19,7 @@ import "../controls"
import "../popups" import "../popups"
import "../panels" import "../panels"
import "../../Wallet" import "../../Wallet"
import "../stores"
Item { Item {
id: root id: root
@ -40,7 +42,6 @@ Item {
readonly property var contactDetails: rootStore ? rootStore.oneToOneChatContact : null readonly property var contactDetails: rootStore ? rootStore.oneToOneChatContact : null
readonly property bool isUserAdded: root.contactDetails && root.contactDetails.isAdded readonly property bool isUserAdded: root.contactDetails && root.contactDetails.isAdded
signal openAppSearch()
signal openStickerPackPopup(string stickerPackId) signal openStickerPackPopup(string stickerPackId)
function requestAddressForTransaction(address, amount, tokenAddress, tokenDecimals = 18) { function requestAddressForTransaction(address, amount, tokenAddress, tokenDecimals = 18) {
@ -98,6 +99,84 @@ Item {
root.createChatPropertiesStore.resetProperties() 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 { EmptyChatPanel {
anchors.fill: parent anchors.fill: parent
visible: root.activeChatId === "" || root.chatsCount == 0 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. // 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. // 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 { ColumnLayout {
width: parent.width anchors.fill: parent
height: parent.height spacing: 0
visible: !root.rootStore.openCreateChat && isActiveChannel
chatId: model.itemId Item {
chatType: model.type Layout.fillWidth: true
chatMessagesLoader.active: model.loaderActive Layout.fillHeight: true
rootStore: root.rootStore
contactsStore: root.contactsStore 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 emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup stickersPopup: root.stickersPopup
sendTransactionNoEnsModal: cmpSendTransactionNoEns isContactBlocked: d.activeChatContentModule.chatDetails.blocked
receiveTransactionModal: cmpReceiveTransaction chatType: root.activeChatType
sendTransactionWithEnsModal: cmpSendTransactionWithEns suggestions.suggestionFilter.addSystemSuggestions: chatType === Constants.chatType.communityChat
stickersLoaded: root.stickersLoaded
isBlocked: model.blocked textInput.onTextChanged: {
isActiveChannel: model.active d.activeChatContentModule.inputAreaModule.preservedProperties.text = textInput.text
onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId)
} }
onOpenAppSearch: {
root.openAppSearch(); onReplyMessageIdChanged: {
d.activeChatContentModule.inputAreaModule.preservedProperties.replyMessageId = replyMessageId
} }
Component.onCompleted: {
parentModule.prepareChatContentModuleForChatId(model.itemId) onFileUrlsAndSourcesChanged: {
chatContentModule = parentModule.getChatContentModule() d.activeChatContentModule.inputAreaModule.preservedProperties.fileUrlsAndSourcesJson = JSON.stringify(chatInput.fileUrlsAndSources)
chatSectionModule = root.parentModule }
root.checkForCreateChatOptions(model.itemId)
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 { Component {
id: cmpSendTransactionNoEns id: cmpSendTransactionNoEns
ChatCommandModal { ChatCommandModal {
id: sendTransactionNoEns
store: root.rootStore store: root.rootStore
contactsStore: root.contactsStore contactsStore: root.contactsStore
onClosed: { onClosed: {
@ -178,7 +361,6 @@ Item {
Component { Component {
id: cmpReceiveTransaction id: cmpReceiveTransaction
ChatCommandModal { ChatCommandModal {
id: receiveTransaction
store: root.rootStore store: root.rootStore
contactsStore: root.contactsStore contactsStore: root.contactsStore
onClosed: { onClosed: {
@ -209,7 +391,6 @@ Item {
Component { Component {
id: cmpSendTransactionWithEns id: cmpSendTransactionWithEns
SendModal { SendModal {
id: sendTransactionWithEns
onClosed: { onClosed: {
destroy() destroy()
} }

View File

@ -24,16 +24,12 @@ import "../stores"
ColumnLayout { ColumnLayout {
id: root id: root
objectName: "chatContentViewColumn"
spacing: 0
// Important: // Important: each chat/channel has its own ChatContentModule
// Each chat/channel has its own ChatContentModule
property var chatContentModule property var chatContentModule
property var chatSectionModule property var chatSectionModule
property var rootStore property var rootStore
property var contactsStore property var contactsStore
property bool isActiveChannel: false
property string chatId property string chatId
property int chatType: Constants.chatType.unknown property int chatType: Constants.chatType.unknown
@ -43,22 +39,27 @@ ColumnLayout {
property var stickersPopup property var stickersPopup
property UsersStore usersStore: UsersStore {} property UsersStore usersStore: UsersStore {}
onChatContentModuleChanged: if (!!chatContentModule) {
root.usersStore.usersModule = root.chatContentModule.usersModule
}
signal openAppSearch()
signal openStickerPackPopup(string stickerPackId) signal openStickerPackPopup(string stickerPackId)
property Component sendTransactionNoEnsModal
property Component receiveTransactionModal
property Component sendTransactionWithEnsModal
property bool isBlocked: false property bool isBlocked: false
property bool isUserAllowedToSendMessage: root.rootStore.isUserAllowedToSendMessage property bool isUserAllowedToSendMessage: root.rootStore.isUserAllowedToSendMessage
property string chatInputPlaceholder: root.rootStore.chatInputPlaceHolderText
property bool stickersLoaded: false 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 { Loader {
Layout.fillWidth: true Layout.fillWidth: true
active: root.isBlocked active: root.isBlocked
@ -69,184 +70,33 @@ ColumnLayout {
} }
} }
readonly property var messageStore: MessageStore { Loader {
messageModule: chatContentModule ? chatContentModule.messagesModule : null id: chatMessagesLoader
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 {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
clip: true
Loader { sourceComponent: ChatMessagesView {
id: chatMessagesLoader chatContentModule: root.chatContentModule
Layout.fillWidth: true rootStore: root.rootStore
Layout.fillHeight: true contactsStore: root.contactsStore
messageStore: root.messageStore
sourceComponent: ChatMessagesView { emojiPopup: root.emojiPopup
chatContentModule: root.chatContentModule stickersPopup: root.stickersPopup
rootStore: root.rootStore usersStore: root.usersStore
contactsStore: root.contactsStore stickersLoaded: root.stickersLoaded
messageStore: root.messageStore chatId: root.chatId
emojiPopup: root.emojiPopup isOneToOne: root.chatType === Constants.chatType.oneToOne
stickersPopup: root.stickersPopup isChatBlocked: root.isBlocked || !root.isUserAllowedToSendMessage
usersStore: root.usersStore channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
stickersLoaded: root.stickersLoaded onShowReplyArea: (messageId, senderId) => {
chatId: root.chatId root.showReplyArea(messageId)
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()
}
} }
} onOpenStickerPackPopup: {
root.openStickerPackPopup(stickerPackId);
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
} }
onEditModeChanged: {
// FIXME: `StatusChatInput` is way too heavy if (!editModeOn)
// see: https://github.com/status-im/status-desktop/pull/10343#issuecomment-1515103756 root.forceInputFocus()
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
})
}
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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