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

682 lines
28 KiB
QML
Raw Normal View History

2020-06-17 19:18:31 +00:00
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import "../../../shared"
import "../../../shared/status"
import "../../../imports"
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"
import "../Wallet"
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: ""
2021-06-30 18:46:26 +00:00
property bool showUsers: false
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-18 17:17:04 +00:00
property alias input: chatInput
2021-05-25 19:38:18 +00:00
property string hoveredMessage
property string activeMessage
property var currentTime: 0
property var idMap: ({})
property var suggestionsObj: ([])
property Timer timer: Timer { }
property var userList
property var onActivated: function () {
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
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 = ""
}
}
2021-02-03 21:38:05 +00:00
function addSuggestionFromMessageList(i){
const contactAddr = chatsModel.messageView.messageList.getMessageData(i, "publicKey");
2021-02-03 21:38:05 +00:00
if(idMap[contactAddr]) return;
suggestionsObj.push({
alias: chatsModel.messageView.messageList.getMessageData(i, "alias"),
ensName: chatsModel.messageView.messageList.getMessageData(i, "ensName"),
address: contactAddr,
identicon: chatsModel.messageView.messageList.getMessageData(i, "identicon"),
localNickname: chatsModel.messageView.messageList.getMessageData(i, "localName")
})
2021-05-18 17:17:04 +00:00
chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
2021-02-03 21:38:05 +00:00
idMap[contactAddr] = true;
}
function populateSuggestions() {
2021-05-18 17:17:04 +00:00
chatInput.suggestionsList.clear()
2021-02-03 21:38:05 +00:00
idMap = {}
let isCommunity = chatsModel.communities.activeCommunity.active
let dataSource = isCommunity ? chatsModel.communities.activeCommunity.members : chatsModel.suggestionList
const len = dataSource.rowCount()
for (let i = 0; i < len; i++) {
const contactAddr = dataSource.rowData(i, "address");
if(idMap[contactAddr]) continue;
suggestionsObj.push({
alias: dataSource.rowData(i, "alias"),
ensName: dataSource.rowData(i, "ensName"),
address: contactAddr,
identicon: getProfileImage(contactAddr, false, false) || dataSource.rowData(i, "identicon"),
localNickname: dataSource.rowData(i, "localNickname")
})
2021-05-18 17:17:04 +00:00
chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
idMap[contactAddr] = true;
}
2021-07-22 14:43:07 +00:00
if (isCommunity) return;
const len2 = chatsModel.messageView.messageList.rowCount();
for (let f = 0; f < len2; f++) {
addSuggestionFromMessageList(f);
}
}
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")
2021-05-18 17:17:04 +00:00
chatInput.showReplyArea(userName, message, identicon)
}
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) {
stackLayoutChatMessages.children[stackLayoutChatMessages.currentIndex].item.scrollToMessage(messageId)
}
Timer {
interval: 60000; // 1 min
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
chatColumnLayout.currentTime = Date.now()
}
}
StackLayout {
anchors.fill: parent
currentIndex: chatsModel.channelView.activeChannelIndex > -1 && chatGroupsListViewCount > 0 ? 0 : 1
StatusImageModal {
id: imagePopup
}
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 chatsModel.channelView.activeChannel.description
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.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne
membersButton.highlighted: showUsers
notificationButton.visible: appSettings.isActivityCenterEnabled
notificationCount: chatsModel.activityNotificationList.unreadCount
onSearchButtonClicked: searchPopup.open()
SearchPopup {
id: searchPopup
}
onMembersButtonClicked: showUsers = !showUsers
onNotificationButtonClicked: activityCenter.open()
popupMenu: ChatContextMenu {
openHandler: {
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
}
}
}
StackLayout {
id: stackLayoutChatMessages
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
currentIndex: chatsModel.messageView.getMessageListIndex(chatsModel.channelView.activeChannelIndex)
Repeater {
model: chatsModel.messageView
Loader {
active: stackLayoutChatMessages.currentIndex === index
sourceComponent: ChatMessages {
id: chatMessages
2021-08-05 21:05:47 +00:00
messageList: messages
messageContextMenuInst: MessageContextMenu {
reactionModel: EmojiReactions { }
}
Component.onCompleted: {
chatColumnLayout.userList = chatMessages.messageList.userList;
}
}
}
}
}
Connections {
target: chatsModel.channelView
onActiveChannelChanged: {
chatInput.suggestions.hide();
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
populateSuggestions();
}
}
Connections {
target: chatsModel.messageView
onMessagePushed: {
addSuggestionFromMessageList(messageIndex);
}
}
Connections {
target: profileModel
onContactsChanged: {
populateSuggestions();
}
}
ChatRequestMessage {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
Layout.bottomMargin: Style.current.bigPadding
}
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
}
enabled: !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()
2021-05-18 17:17:04 +00:00
}
onReceiveTransactionCommandButtonClicked: {
txModalLoader.sourceComponent = cmpReceiveTransaction
txModalLoader.item.open()
}
onStickerSelected: {
chatsModel.stickers.send(hashId, 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, JSON.stringify(suggestionsObj));
if(event) event.accepted = true
sendMessageSound.stop();
Qt.callLater(sendMessageSound.play);
chatInput.textInput.clear();
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
}
}
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: {
walletModel.gasView.getGasPricePredictions()
}
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);
}
onContactBlocked: {
chatsModel.messageView.removeMessagesByUserId(publicKey)
}
}
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)
}
onMessageNotificationPushed: function(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
if (appSettings.useOSNotifications && systemTray.supportsMessages) {
systemTray.showMessage(name,
message,
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
} else {
notificationWindow.notifyUser(chatId, name, message, chatType, identicon, chatColumnLayout.clickOnNotification)
}
}
}
}
Component.onCompleted: {
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
Connections {
target: chatsModel.stickers
onTransactionWasSent: {
2021-07-30 16:02:22 +00:00
//% "Transaction pending"
toastMessage.title = qsTrId("transaction-pending")
toastMessage.source = "../../../img/loading.svg"
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 = "../../../img/check-circle.svg"
toastMessage.iconColor = Style.current.success
} else {
toastMessage.source = "../../../img/block-icon.svg"
toastMessage.iconColor = Style.current.danger
}
toastMessage.link = `${walletModel.utilsView.etherscanLink}/${txHash}`
toastMessage.open()
}
}
}
}
/*##^##
Designer {
D{i:0;formeditorColor:"#ffffff";height:770;width:800}
}
##^##*/