status-desktop/ui/app/AppLayouts/Chat/ChatColumn.qml

674 lines
30 KiB
QML
Raw Normal View History

2020-06-17 19:18:31 +00:00
import QtQuick 2.13
import Qt.labs.platform 1.1
2020-06-17 19:18:31 +00:00
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.0
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import "../../../shared"
import "../../../shared/status"
import "../../../shared/popups"
import utils 1.0
2020-06-25 16:27:46 +00:00
import "./components"
2020-05-27 21:59:34 +00:00
import "./ChatColumn"
import "./ChatColumn/ChatComponents"
import "./data"
Item {
id: chatColumnLayout
anchors.fill: parent
2021-05-25 19:38:18 +00:00
property alias pinnedMessagesPopupComponent: pinnedMessagesPopupComponent
property int chatGroupsListViewCount: 0
2020-07-09 17:47:36 +00:00
property bool isReply: false
property bool isImage: false
property bool isExtendedInput: isReply || isImage
property bool isConnected: false
property string contactToRemove: ""
property string activeChatId: chatsModel.channelView.activeChannel.id
property bool isBlocked: profileModel.contacts.isContactBlocked(activeChatId)
property bool isContact: profileModel.contacts.isAdded(activeChatId)
property bool contactRequestReceived: profileModel.contacts.contactRequestReceived(activeChatId)
property string currentNotificationChatId
property string currentNotificationCommunityId
2021-05-25 19:38:18 +00:00
property string hoveredMessage
property string activeMessage
property var currentTime: 0
property var idMap: ({})
property Timer timer: Timer { }
property var userList
property var onActivated: function () {
if(stackLayoutChatMessages.currentIndex >= 0 && stackLayoutChatMessages.currentIndex < stackLayoutChatMessages.children.length)
stackLayoutChatMessages.children[stackLayoutChatMessages.currentIndex].chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
function hideChatInputExtendedArea () {
if(stackLayoutChatMessages.currentIndex >= 0 && stackLayoutChatMessages.currentIndex < stackLayoutChatMessages.children.length)
stackLayoutChatMessages.children[stackLayoutChatMessages.currentIndex].chatInput.hideExtendedArea()
}
2021-05-25 19:38:18 +00:00
function setHovered(messageId, hovered) {
if (hovered) {
hoveredMessage = messageId
} else if (hoveredMessage === messageId) {
hoveredMessage = ""
}
}
function setMessageActive(messageId, active) {
if (active) {
activeMessage = messageId
} else if (activeMessage === messageId) {
activeMessage = ""
}
}
function showReplyArea() {
isReply = true;
isImage = false;
let replyMessageIndex = chatsModel.messageView.messageList.getMessageIndex(SelectedMessage.messageId);
if (replyMessageIndex === -1) return;
let userName = chatsModel.messageView.messageList.getMessageData(replyMessageIndex, "userName")
let message = chatsModel.messageView.messageList.getMessageData(replyMessageIndex, "message")
let identicon = chatsModel.messageView.messageList.getMessageData(replyMessageIndex, "identicon")
let image = chatsModel.messageView.messageList.getMessageData(replyMessageIndex, "image")
let sticker = chatsModel.messageView.messageList.getMessageData(replyMessageIndex, "sticker")
let contentType = chatsModel.messageView.messageList.getMessageData(replyMessageIndex, "contentType")
if(stackLayoutChatMessages.currentIndex >= 0 && stackLayoutChatMessages.currentIndex < stackLayoutChatMessages.children.length)
stackLayoutChatMessages.children[stackLayoutChatMessages.currentIndex].chatInput.showReplyArea(userName, message, identicon, contentType, image, sticker)
}
function requestAddressForTransaction(address, amount, tokenAddress, tokenDecimals = 18) {
amount = utilsModel.eth2Wei(amount.toString(), tokenDecimals)
chatsModel.transactions.requestAddress(activeChatId,
address,
amount,
tokenAddress)
txModalLoader.close()
}
function requestTransaction(address, amount, tokenAddress, tokenDecimals = 18) {
amount = utilsModel.eth2Wei(amount.toString(), tokenDecimals)
chatsModel.transactions.request(activeChatId,
address,
amount,
tokenAddress)
txModalLoader.close()
}
function clickOnNotification() {
// So far we're just showing this app as the top most window. Once we decide about the way
// how to notify the app what channle should be displayed within the app when user clicks
// notificaiton bubble this part should be updated accordingly.
//
// I removed part of this function which caused app crash.
applicationWindow.show()
applicationWindow.raise()
applicationWindow.requestActivate()
}
function positionAtMessage(messageId, isSearch = false) {
stackLayoutChatMessages.children[stackLayoutChatMessages.currentIndex].message.scrollToMessage(messageId, isSearch);
}
Timer {
interval: 60000; // 1 min
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
chatColumnLayout.currentTime = Date.now()
}
}
MessageContextMenu {
id: contextmenu
reactionModel: EmojiReactions { }
}
StackLayout {
anchors.fill: parent
currentIndex: chatsModel.channelView.activeChannelIndex > -1 && chatGroupsListViewCount > 0 ? 0 : 1
StatusImageModal {
id: imagePopup
onClicked: {
if (button === Qt.LeftButton) {
imagePopup.close()
}
else if(button === Qt.RightButton) {
contextmenu.imageSource = imagePopup.imageSource
contextmenu.hideEmojiPicker = true
contextmenu.isRightClickOnImage = true;
contextmenu.show()
}
}
}
ColumnLayout {
spacing: 0
StatusChatToolBar {
id: topBar
Layout.fillWidth: true
property string chatId: chatsModel.channelView.activeChannel.id
property string profileImage: appMain.getProfileImage(chatId) || ""
chatInfoButton.title: Utils.removeStatusEns(chatsModel.channelView.activeChannel.name)
chatInfoButton.subTitle: {
switch (chatsModel.channelView.activeChannel.chatType) {
case Constants.chatTypeOneToOne:
return (profileModel.contacts.isAdded(topBar.chatId) ?
//% "Contact"
qsTrId("chat-is-a-contact") :
//% "Not a contact"
qsTrId("chat-is-not-a-contact"))
case Constants.chatTypePublic:
//% "Public chat"
return qsTrId("public-chat")
case Constants.chatTypePrivateGroupChat:
let cnt = chatsModel.channelView.activeChannel.members.rowCount();
//% "%1 members"
if(cnt > 1) return qsTrId("-1-members").arg(cnt);
//% "1 member"
return qsTrId("1-member");
case Constants.chatTypeCommunity:
return Utils.linkifyAndXSS(chatsModel.channelView.activeChannel.description).trim()
default:
return ""
}
}
chatInfoButton.image.source: profileImage || chatsModel.channelView.activeChannel.identicon
chatInfoButton.image.isIdenticon: !!!profileImage && chatsModel.channelView.activeChannel.identicon
chatInfoButton.icon.color: chatsModel.channelView.activeChannel.color
chatInfoButton.type: chatsModel.channelView.activeChannel.chatType
chatInfoButton.pinnedMessagesCount: chatsModel.messageView.pinnedMessagesList.count
chatInfoButton.muted: chatsModel.channelView.activeChannel.muted
chatInfoButton.onPinnedMessagesCountClicked: openPopup(pinnedMessagesPopupComponent)
chatInfoButton.onUnmute: chatsModel.channelView.unmuteChatItem(chatsModel.channelView.activeChannel.id)
chatInfoButton.sensor.enabled: chatsModel.channelView.activeChannel.chatType !== Constants.chatTypePublic &&
chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeCommunity
chatInfoButton.onClicked: {
switch (chatsModel.channelView.activeChannel.chatType) {
case Constants.chatTypePrivateGroupChat:
openPopup(groupInfoPopupComponent, {
channelType: GroupInfoPopup.ChannelType.ActiveChannel,
channel: chatsModel.channelView.activeChannel
})
break;
case Constants.chatTypeOneToOne:
openProfilePopup(chatsModel.userNameOrAlias(chatsModel.channelView.activeChannel.id),
chatsModel.channelView.activeChannel.id, profileImage || chatsModel.channelView.activeChannel.identicon,
"", chatsModel.channelView.activeChannel.nickname)
break;
}
}
membersButton.visible: (appSettings.showOnlineUsers || chatsModel.communities.activeCommunity.active)
&& chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne
membersButton.highlighted: appSettings.expandUsersList
notificationButton.visible: appSettings.isActivityCenterEnabled
notificationButton.tooltip.offset: appSettings.expandUsersList ? 0 : 14
notificationCount: chatsModel.activityNotificationList.unreadCount
onSearchButtonClicked: searchPopup.open()
onMembersButtonClicked: appSettings.expandUsersList = !appSettings.expandUsersList
onNotificationButtonClicked: activityCenter.open()
popupMenu: ChatContextMenu {
onOpened: {
chatItem = chatsModel.channelView.activeChannel
}
}
}
Rectangle {
id: connectedStatusRect
Layout.fillWidth: true
height: 40
Layout.alignment: Qt.AlignHCenter
z: 60
visible: false
color: isConnected ? Style.current.green : Style.current.darkGrey
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: Style.current.white
id: connectedStatusLbl
text: isConnected ?
//% "Connected"
qsTrId("connected") :
//% "Disconnected"
qsTrId("disconnected")
}
Connections {
target: chatsModel
onOnlineStatusChanged: {
if (connected == isConnected) return;
isConnected = connected;
if(isConnected){
timer.setTimeout(function(){
connectedStatusRect.visible = false;
}, 5000);
} else {
connectedStatusRect.visible = true;
}
}
}
Component.onCompleted: {
isConnected = chatsModel.isOnline
if(!isConnected){
connectedStatusRect.visible = true
}
}
}
2021-08-20 04:41:56 +00:00
Item {
Layout.fillWidth: true
2021-08-20 04:41:56 +00:00
Layout.preferredHeight: 40
Layout.alignment: Qt.AlignHCenter
visible: isBlocked
2021-08-20 04:41:56 +00:00
Rectangle {
id: blockedBanner
anchors.fill: parent
color: Style.current.red
opacity: 0.1
}
2021-08-17 16:31:52 +00:00
Text {
id: blockedText
anchors.centerIn: blockedBanner
color: Style.current.red
text: qsTr("Blocked")
}
}
StackLayout {
id: stackLayoutChatMessages
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
currentIndex: chatsModel.messageView.getMessageListIndex(chatsModel.channelView.activeChannelIndex)
Repeater {
model: chatsModel.messageView
ColumnLayout {
property alias chatInput: chatInput
property alias message: messageLoader.item
Loader {
id: messageLoader
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: parent.width
active: stackLayoutChatMessages.currentIndex === index
sourceComponent: ChatMessages {
id: chatMessages
messageList: messages
messageContextMenuInst: contextmenu
Component.onCompleted: {
chatColumnLayout.userList = chatMessages.messageList.userList;
}
}
}
Item {
id: inputArea
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
Layout.preferredWidth: parent.width
height: chatInput.height
Layout.preferredHeight: height
Connections {
target: chatsModel.messageView
onLoadingMessagesChanged:
if(value){
loadingMessagesIndicator.active = true
} else {
timer.setTimeout(function(){
loadingMessagesIndicator.active = false;
}, 5000);
}
}
Loader {
id: loadingMessagesIndicator
active: chatsModel.messageView.loadingMessages
sourceComponent: loadingIndicator
anchors.right: parent.right
anchors.bottom: chatInput.top
anchors.rightMargin: Style.current.padding
anchors.bottomMargin: Style.current.padding
}
Component {
id: loadingIndicator
LoadingAnimation { }
}
StatusChatInput {
id: chatInput
visible: {
if (chatsModel.channelView.activeChannel.chatType === Constants.chatTypePrivateGroupChat) {
return chatsModel.channelView.activeChannel.isMember
}
if (chatsModel.channelView.activeChannel.chatType === Constants.chatTypeOneToOne) {
return isContact
}
const community = chatsModel.communities.activeCommunity
return !community.active ||
community.access === Constants.communityChatPublicAccess ||
community.admin ||
chatsModel.channelView.activeChannel.canPost
}
isContactBlocked: isBlocked
chatInputPlaceholder: isBlocked ?
//% "This user has been blocked."
qsTrId("this-user-has-been-blocked-") :
//% "Type a message."
qsTrId("type-a-message-")
anchors.bottom: parent.bottom
recentStickers: chatsModel.stickers.recent
stickerPackList: chatsModel.stickers.stickerPacks
chatType: chatsModel.channelView.activeChannel.chatType
onSendTransactionCommandButtonClicked: {
if (chatsModel.channelView.activeChannel.ensVerified) {
txModalLoader.sourceComponent = cmpSendTransactionWithEns
} else {
txModalLoader.sourceComponent = cmpSendTransactionNoEns
}
txModalLoader.item.open()
}
onReceiveTransactionCommandButtonClicked: {
txModalLoader.sourceComponent = cmpReceiveTransaction
txModalLoader.item.open()
}
onStickerSelected: {
chatsModel.stickers.send(hashId, chatInput.isReply ? SelectedMessage.messageId : "", packId)
}
onSendMessage: {
if (chatInput.fileUrls.length > 0){
chatsModel.sendImages(JSON.stringify(fileUrls));
}
let msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text))
if (msg.length > 0){
msg = chatInput.interpretMessage(msg)
chatsModel.messageView.sendMessage(msg, chatInput.isReply ? SelectedMessage.messageId : "", Utils.isOnlyEmoji(msg) ? Constants.emojiType : Constants.messageType, false);
if(event) event.accepted = true
sendMessageSound.stop();
Qt.callLater(sendMessageSound.play);
chatInput.textInput.clear();
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
}
}
}
Connections {
target: chatsModel.channelView
onActiveChannelChanged: {
isBlocked = profileModel.contacts.isContactBlocked(activeChatId);
chatInput.suggestions.hide();
if(stackLayoutChatMessages.currentIndex >= 0 && stackLayoutChatMessages.currentIndex < stackLayoutChatMessages.children.length)
stackLayoutChatMessages.children[stackLayoutChatMessages.currentIndex].chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
}
}
}
}
ChatRequestMessage {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
Layout.bottomMargin: Style.current.bigPadding
2021-05-18 17:17:04 +00:00
}
}
2021-05-18 17:17:04 +00:00
EmptyChat { }
2021-05-18 17:17:04 +00:00
Loader {
id: txModalLoader
function close() {
if (!this.item) {
return
}
this.item.close()
this.closed()
2021-05-18 17:17:04 +00:00
}
function closed() {
this.sourceComponent = undefined
}
}
2021-05-18 17:17:04 +00:00
Component {
id: cmpSendTransactionNoEns
ChatCommandModal {
id: sendTransactionNoEns
onClosed: {
txModalLoader.closed()
2021-05-18 17:17:04 +00:00
}
sendChatCommand: chatColumnLayout.requestAddressForTransaction
isRequested: false
//% "Send"
commandTitle: qsTrId("command-button-send")
title: commandTitle
//% "Request Address"
finalButtonLabel: qsTrId("request-address")
selectRecipient.selectedRecipient: {
return {
address: Constants.zeroAddress, // Setting as zero address since we don't have the address yet
alias: chatsModel.channelView.activeChannel.alias,
identicon: chatsModel.channelView.activeChannel.identicon,
name: chatsModel.channelView.activeChannel.name,
type: RecipientSelector.Type.Contact
2021-05-18 17:17:04 +00:00
}
}
selectRecipient.selectedType: RecipientSelector.Type.Contact
selectRecipient.readOnly: true
}
}
Component {
id: cmpReceiveTransaction
ChatCommandModal {
id: receiveTransaction
onClosed: {
txModalLoader.closed()
2021-05-18 17:17:04 +00:00
}
sendChatCommand: chatColumnLayout.requestTransaction
isRequested: true
//% "Request"
commandTitle: qsTrId("wallet-request")
title: commandTitle
//% "Request"
finalButtonLabel: qsTrId("wallet-request")
selectRecipient.selectedRecipient: {
return {
address: Constants.zeroAddress, // Setting as zero address since we don't have the address yet
alias: chatsModel.channelView.activeChannel.alias,
identicon: chatsModel.channelView.activeChannel.identicon,
name: chatsModel.channelView.activeChannel.name,
type: RecipientSelector.Type.Contact
2021-05-18 17:17:04 +00:00
}
}
selectRecipient.selectedType: RecipientSelector.Type.Contact
selectRecipient.readOnly: true
2021-05-18 17:17:04 +00:00
}
}
Component {
id: cmpSendTransactionWithEns
SendModal {
id: sendTransactionWithEns
onOpened: {
2021-07-05 12:34:56 +00:00
walletModel.gasView.getGasPrice()
}
onClosed: {
txModalLoader.closed()
}
selectRecipient.readOnly: true
selectRecipient.selectedRecipient: {
return {
address: "",
alias: chatsModel.channelView.activeChannel.alias,
identicon: chatsModel.channelView.activeChannel.identicon,
name: chatsModel.channelView.activeChannel.name,
type: RecipientSelector.Type.Contact,
ensVerified: true
}
}
selectRecipient.selectedType: RecipientSelector.Type.Contact
}
}
ActivityCenter {
id: activityCenter
height: chatColumnLayout.height - (topBar.height * 2) // TODO get screen size
y: topBar.height
}
Connections {
target: profileModel.contacts
onContactListChanged: {
isBlocked = profileModel.contacts.isContactBlocked(activeChatId);
}
}
Connections {
target: chatsModel.channelView
onActiveChannelChanged: {
chatsModel.messageView.hideLoadingIndicator()
SelectedMessage.reset();
chatColumn.isReply = false;
}
}
Connections {
target: systemTray
onMessageClicked: function () {
clickOnNotification()
}
}
Component {
id: pinnedMessagesPopupComponent
PinnedMessagesPopup {
id: pinnedMessagesPopup
onClosed: destroy()
}
}
Connections {
target: chatsModel.messageView
onSearchedMessageLoaded: {
positionAtMessage(messageId, true);
}
onMessageNotificationPushed: function(messageId, communityId, chatId, msg, contentType, chatType, timestamp, identicon, username, hasMention, isAddedContact, channelName) {
if (appSettings.notificationSetting == Constants.notifyAllMessages ||
(appSettings.notificationSetting == Constants.notifyJustMentions && hasMention)) {
if (chatId === chatsModel.channelView.activeChannel.id && applicationWindow.active === true) {
// Do not show the notif if we are in the channel already and the window is active and focused
return
}
chatColumnLayout.currentNotificationChatId = chatId
chatColumnLayout.currentNotificationCommunityId = null
let name;
if (appSettings.notificationMessagePreviewSetting === Constants.notificationPreviewAnonymous) {
name = "Status"
} else if (chatType === Constants.chatTypePublic) {
name = chatId
} else {
name = chatType === Constants.chatTypePrivateGroupChat ? Utils.filterXSS(channelName) : Utils.removeStatusEns(username)
}
let message;
if (appSettings.notificationMessagePreviewSetting > Constants.notificationPreviewNameOnly) {
switch(contentType){
//% "Image"
case Constants.imageType: message = qsTrId("image"); break
//% "Sticker"
case Constants.stickerType: message = qsTrId("sticker"); break
default: message = msg // don't parse emojis here as it emits HTML
}
} else {
//% "You have a new message"
message = qsTrId("you-have-a-new-message")
}
currentlyHasANotification = true
// Note:
// Show notification should be moved to the nim side.
// Left here only cause we don't have a way to deal with translations on the nim side.
chatsModel.showOSNotification(name,
message,
Constants.osNotificationType.newMessage,
communityId,
chatId,
messageId,
appSettings.useOSNotifications)
}
}
}
Component.onCompleted: {
if(stackLayoutChatMessages.currentIndex >= 0 && stackLayoutChatMessages.currentIndex < stackLayoutChatMessages.children.length)
stackLayoutChatMessages.children[stackLayoutChatMessages.currentIndex].chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
Connections {
target: chatsModel.stickers
onTransactionWasSent: {
2021-07-05 12:34:56 +00:00
//% "Transaction pending..."
toastMessage.title = qsTr("Transaction pending...")
toastMessage.source = Style.svg("loading")
toastMessage.iconColor = Style.current.primary
toastMessage.iconRotates = true
toastMessage.link = `${walletModel.utilsView.etherscanLink}/${txResult}`
toastMessage.open()
}
onTransactionCompleted: {
toastMessage.title = !success ?
//% "Could not buy Stickerpack"
qsTrId("could-not-buy-stickerpack")
:
//% "Stickerpack bought successfully"
qsTrId("stickerpack-bought-successfully");
if (success) {
toastMessage.source = Style.svg("check-circle")
toastMessage.iconColor = Style.current.success
} else {
toastMessage.source = Style.svg("block-icon")
toastMessage.iconColor = Style.current.danger
}
toastMessage.link = `${walletModel.utilsView.etherscanLink}/${txHash}`
toastMessage.open()
}
}
}
}
/*##^##
Designer {
D{i:0;formeditorColor:"#ffffff";height:770;width:800}
}
##^##*/