feat: add contact requests and handling of them

This commit is contained in:
Jonathan Rainville 2021-05-26 13:36:24 -04:00
parent 03addd4ea9
commit 436cb42eae
19 changed files with 672 additions and 207 deletions

View File

@ -74,7 +74,7 @@ QtObject:
proc pendingRequestsToJoinForCommunity*(self: CommunitiesView, communityId: string): seq[CommunityMembershipRequest] =
result = self.status.chat.pendingRequestsToJoinForCommunity(communityId)
proc membershipRequestPushed*(self: CommunitiesView, communityName: string, pubKey: string) {.signal.}
proc membershipRequestPushed*(self: CommunitiesView, communityId: string, communityName: string, pubKey: string) {.signal.}
proc addMembershipRequests*(self: CommunitiesView, membershipRequests: seq[CommunityMembershipRequest]) =
var communityId: string
@ -87,7 +87,7 @@ QtObject:
let alreadyPresentRequestIdx = community.membershipRequests.findIndexById(request.id)
if (alreadyPresentRequestIdx == -1):
community.membershipRequests.add(request)
self.membershipRequestPushed(community.name, request.publicKey)
self.membershipRequestPushed(community.id, community.name, request.publicKey)
else:
community.membershipRequests[alreadyPresentRequestIdx] = request
self.joinedCommunityList.replaceCommunity(community)
@ -171,7 +171,7 @@ QtObject:
error "Error joining the community", msg = e.msg
result = fmt"Error joining the community: {e.msg}"
proc membershipRequestChanged*(self: CommunitiesView, communityName: string, accepted: bool) {.signal.}
proc membershipRequestChanged*(self: CommunitiesView, communityId: string, communityName: string, accepted: bool) {.signal.}
proc communityAdded*(self: CommunitiesView, communityId: string) {.signal.}
@ -200,7 +200,7 @@ QtObject:
var i = 0
for communityRequest in self.myCommunityRequests:
if (communityRequest.communityId == community.id):
self.membershipRequestChanged(community.name, true)
self.membershipRequestChanged(community.id, community.name, true)
self.myCommunityRequests.delete(i, i)
break
i = i + 1

View File

@ -49,6 +49,8 @@ QtObject:
proc activeChanged*(self: CommunityItemView) {.signal.}
proc setActive*(self: CommunityItemView, value: bool) {.slot.} =
if (self.active == value):
return
self.active = value
self.status.events.emit("communityActiveChanged", CommunityActiveChangedArgs(active: value))
self.activeChanged()

View File

@ -120,6 +120,7 @@ proc init*(self: ProfileController, account: Account) =
# TODO: view should react to model changes
self.status.chat.updateContacts(msgData.contacts)
self.view.contacts.updateContactList(msgData.contacts)
self.view.contacts.notifyOnNewContactRequests(msgData.contacts)
if msgData.installations.len > 0:
self.view.devices.addDevices(msgData.installations)

View File

@ -16,6 +16,7 @@ type
LocalNickname = UserRole + 9
ThumbnailImage = UserRole + 10
LargeImage = UserRole + 11
RequestReceived = UserRole + 12
QtObject:
type ContactList* = ref object of QAbstractListModel
@ -38,6 +39,15 @@ QtObject:
method rowCount(self: ContactList, index: QModelIndex = nil): int =
return self.contacts.len
proc countChanged*(self: ContactList) {.signal.}
proc count*(self: ContactList): int {.slot.} =
self.contacts.len
QtProperty[int] count:
read = count
notify = countChanged
proc userName*(self: ContactList, pubKey: string, defaultValue: string = ""): string {.slot.} =
for contact in self.contacts:
if(contact.id != pubKey): continue
@ -66,6 +76,7 @@ QtObject:
of "localNickname": result = $contact.localNickname
of "thumbnailImage": result = $contact.identityImage.thumbnail
of "largeImage": result = $contact.identityImage.large
of "requestReceived": result = $contact.requestReceived()
method data(self: ContactList, index: QModelIndex, role: int): QVariant =
if not index.isValid:
@ -85,6 +96,7 @@ QtObject:
of ContactRoles.LocalNickname: result = newQVariant(contact.localNickname)
of ContactRoles.ThumbnailImage: result = newQVariant(contact.identityImage.thumbnail)
of ContactRoles.LargeImage: result = newQVariant(contact.identityImage.large)
of ContactRoles.RequestReceived: result = newQVariant(contact.requestReceived())
method roleNames(self: ContactList): Table[int, string] =
{
@ -98,13 +110,15 @@ QtObject:
ContactRoles.LocalNickname.int:"localNickname",
ContactRoles.EnsVerified.int:"ensVerified",
ContactRoles.ThumbnailImage.int:"thumbnailImage",
ContactRoles.LargeImage.int:"largeImage"
ContactRoles.LargeImage.int:"largeImage",
ContactRoles.RequestReceived.int:"requestReceived"
}.toTable
proc addContactToList*(self: ContactList, contact: Profile) =
self.beginInsertRows(newQModelIndex(), self.contacts.len, self.contacts.len)
self.contacts.add(contact)
self.endInsertRows()
self.countChanged()
proc hasAddedContacts(self: ContactList): bool {.slot.} =
for c in self.contacts:
@ -134,3 +148,4 @@ QtObject:
self.beginResetModel()
self.contacts = contactList
self.endResetModel()
self.countChanged()

View File

@ -1,4 +1,4 @@
import NimQml, chronicles, sequtils, sugar, strutils
import NimQml, chronicles, sequtils, sugar, strutils, json
import ../../../status/libstatus/utils as status_utils
import ../../../status/status
import ../../../status/chat/chat
@ -34,6 +34,7 @@ QtObject:
type ContactsView* = ref object of QObject
status: Status
contactList*: ContactList
contactRequests*: ContactList
addedContacts*: ContactList
blockedContacts*: ContactList
contactToAdd*: Profile
@ -44,6 +45,7 @@ QtObject:
proc delete*(self: ContactsView) =
self.contactList.delete
self.addedContacts.delete
self.contactRequests.delete
self.blockedContacts.delete
self.QObject.delete
@ -51,6 +53,7 @@ QtObject:
new(result, delete)
result.status = status
result.contactList = newContactList()
result.contactRequests = newContactList()
result.addedContacts = newContactList()
result.blockedContacts = newContactList()
result.contactToAdd = Profile(
@ -63,10 +66,12 @@ QtObject:
proc updateContactList*(self: ContactsView, contacts: seq[Profile]) =
for contact in contacts:
self.contactList.updateContact(contact)
if contact.systemTags.contains(":contact/added"):
self.addedContacts.updateContact(contact)
if contact.systemTags.contains(":contact/blocked"):
self.blockedContacts.updateContact(contact)
if contact.systemTags.contains(contactAdded):
self.addedContacts.updateContact(contact)
if contact.systemTags.contains(contactBlocked):
self.blockedContacts.updateContact(contact)
if contact.systemTags.contains(contactRequest) and not contact.systemTags.contains(contactAdded) and not contact.systemTags.contains(contactBlocked):
self.contactRequests.updateContact(contact)
proc contactListChanged*(self: ContactsView) {.signal.}
@ -75,10 +80,18 @@ QtObject:
proc setContactList*(self: ContactsView, contactList: seq[Profile]) =
self.contactList.setNewData(contactList)
self.addedContacts.setNewData(contactList.filter(c => c.systemTags.contains(":contact/added")))
self.blockedContacts.setNewData(contactList.filter(c => c.systemTags.contains(":contact/blocked")))
self.addedContacts.setNewData(contactList.filter(c => c.systemTags.contains(contactAdded)))
self.blockedContacts.setNewData(contactList.filter(c => c.systemTags.contains(contactBlocked)))
self.contactRequests.setNewData(contactList.filter(c => c.systemTags.contains(contactRequest) and not c.systemTags.contains(contactAdded) and not c.systemTags.contains(contactBlocked)))
self.contactListChanged()
proc contactRequestAdded*(self: ContactsView, name: string, address: string) {.signal.}
proc notifyOnNewContactRequests*(self: ContactsView, contacts: seq[Profile]) =
for contact in contacts:
if contact.systemTags.contains(contactRequest) and not contact.systemTags.contains(contactAdded) and not contact.systemTags.contains(contactBlocked):
self.contactRequestAdded(status_ens.userNameOrAlias(contact), contact.address)
QtProperty[QVariant] list:
read = getContactList
write = setContactList
@ -104,6 +117,13 @@ QtObject:
return true
return false
proc getContactRequests(self: ContactsView): QVariant {.slot.} =
return newQVariant(self.contactRequests)
QtProperty[QVariant] contactRequests:
read = getContactRequests
notify = contactListChanged
proc contactToAddChanged*(self: ContactsView) {.signal.}
proc getContactToAddUsername(self: ContactsView): QVariant {.slot.} =
@ -129,6 +149,10 @@ QtObject:
if id == "": return false
self.status.contacts.isAdded(id)
proc contactRequestReceived*(self: ContactsView, id: string): bool {.slot.} =
if id == "": return false
self.status.contacts.contactRequestReceived(id)
proc lookupContact*(self: ContactsView, value: string) {.slot.} =
if value == "":
return
@ -164,6 +188,19 @@ QtObject:
self.status.chat.join(status_utils.getTimelineChatId(publicKey), ChatType.Profile, "", publicKey)
self.contactChanged(publicKey, true)
proc rejectContactRequest*(self: ContactsView, publicKey: string) {.slot.} =
self.status.contacts.rejectContactRequest(publicKey)
proc rejectContactRequests*(self: ContactsView, publicKeysJSON: string) {.slot.} =
let publicKeys = publicKeysJSON.parseJson
for pubkey in publicKeys:
self.rejectContactRequest(pubkey.getStr)
proc acceptContactRequests*(self: ContactsView, publicKeysJSON: string) {.slot.} =
let publicKeys = publicKeysJSON.parseJson
for pubkey in publicKeys:
discard self.addContact(pubkey.getStr)
proc changeContactNickname*(self: ContactsView, publicKey: string, nickname: string) {.slot.} =
var nicknameToSet = nickname
if (nicknameToSet == ""):

View File

@ -1,4 +1,4 @@
import json, sequtils, sugar
import json, sequtils, sugar, chronicles
import libstatus/contacts as status_contacts
import libstatus/accounts as status_accounts
import libstatus/chat as status_chat
@ -33,13 +33,17 @@ proc getContactByID*(self: ContactModel, id: string): Profile =
proc blockContact*(self: ContactModel, id: string): string =
var contact = self.getContactByID(id)
contact.systemTags.add(":contact/blocked")
contact.systemTags.add(contactBlocked)
let index = contact.systemTags.find(contactAdded)
if (index > -1):
contact.systemTags.delete(index)
discard status_contacts.blockContact(contact)
self.events.emit("contactBlocked", Args())
proc unblockContact*(self: ContactModel, id: string): string =
var contact = self.getContactByID(id)
contact.systemTags.delete(contact.systemTags.find(":contact/blocked"))
contact.systemTags.delete(contact.systemTags.find(contactBlocked))
discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, contact.identityImage.thumbnail, contact.systemTags, contact.localNickname)
self.events.emit("contactUnblocked", Args())
@ -47,7 +51,7 @@ proc getAllContacts*(): seq[Profile] =
result = map(status_contacts.getContacts().getElems(), proc(x: JsonNode): Profile = x.toProfileModel())
proc getAddedContacts*(): seq[Profile] =
result = getAllContacts().filter(c => c.systemTags.contains(":contact/added"))
result = getAllContacts().filter(c => c.systemTags.contains(contactAdded))
proc getContacts*(self: ContactModel): seq[Profile] =
result = getAllContacts()
@ -89,10 +93,16 @@ proc setNickName*(self: ContactModel, id: string, localNickname: string): string
proc addContact*(self: ContactModel, id: string): string =
var contact = self.getOrCreateContact(id)
let updating = contact.systemTags.contains(":contact/added")
let updating = contact.systemTags.contains(contactAdded)
if not updating:
contact.systemTags.add(":contact/added")
contact.systemTags.add(contactAdded)
discard status_chat.createProfileChat(contact.id)
else:
let index = contact.systemTags.find(contactBlocked)
if (index > -1):
contact.systemTags.delete(index)
var thumbnail = ""
if contact.identityImage != nil:
thumbnail = contact.identityImage.thumbnail
@ -117,7 +127,7 @@ proc addContact*(self: ContactModel, id: string): string =
proc removeContact*(self: ContactModel, id: string) =
let contact = self.getContactByID(id)
contact.systemTags.delete(contact.systemTags.find(":contact/added"))
contact.systemTags.delete(contact.systemTags.find(contactAdded))
var thumbnail = ""
if contact.identityImage != nil:
@ -129,4 +139,20 @@ proc removeContact*(self: ContactModel, id: string) =
proc isAdded*(self: ContactModel, id: string): bool =
var contact = self.getContactByID(id)
if contact.isNil: return false
contact.systemTags.contains(":contact/added")
contact.systemTags.contains(contactAdded)
proc contactRequestReceived*(self: ContactModel, id: string): bool =
var contact = self.getContactByID(id)
if contact.isNil: return false
contact.systemTags.contains(contactRequest)
proc rejectContactRequest*(self: ContactModel, id: string) =
let contact = self.getContactByID(id)
contact.systemTags.delete(contact.systemTags.find(contactRequest))
var thumbnail = ""
if contact.identityImage != nil:
thumbnail = contact.identityImage.thumbnail
discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, thumbnail, contact.systemTags, contact.localNickname)
self.events.emit("contactRemoved", Args())

View File

@ -8,11 +8,19 @@ type Profile* = ref object
appearance*: int
systemTags*: seq[string]
const contactAdded* = ":contact/added"
const contactBlocked* = ":contact/blocked"
const contactRequest* = ":contact/request-received"
proc isContact*(self: Profile): bool =
result = self.systemTags.contains(":contact/added") and not self.systemTags.contains(":contact/blocked")
result = self.systemTags.contains(contactAdded) and not self.systemTags.contains(":contact/blocked")
proc isBlocked*(self: Profile): bool =
result = self.systemTags.contains(":contact/blocked")
result = self.systemTags.contains(contactBlocked)
proc requestReceived*(self: Profile): bool =
result = self.systemTags.contains(contactRequest)
proc toProfileModel*(account: Account): Profile =
result = Profile(

View File

@ -29,13 +29,17 @@ StackLayout {
property var doNotShowAddToContactBannerToThose: ([])
property var onActivated: function () {
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
inputArea.chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
property string activeChatId: chatsModel.activeChannel.id
property bool isBlocked: profileModel.contacts.isContactBlocked(activeChatId)
property bool isContact: profileModel.contacts.isAdded(activeChatId)
property alias input: chatInput
property alias input: inputArea.chatInput
property string currentNotificationChatId
property string currentNotificationCommunityId
property string hoveredMessage
property string activeMessage
@ -57,7 +61,7 @@ StackLayout {
}
Component.onCompleted: {
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
inputArea.chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
Layout.fillHeight: true
@ -95,12 +99,12 @@ StackLayout {
identicon: chatsModel.messageList.getMessageData(i, "identicon"),
localNickname: chatsModel.messageList.getMessageData(i, "localName")
})
chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
inputArea.chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
idMap[contactAddr] = true;
}
function populateSuggestions() {
chatInput.suggestionsList.clear()
inputArea.chatInput.suggestionsList.clear()
const len = chatsModel.suggestionList.rowCount()
idMap = {}
@ -116,7 +120,7 @@ StackLayout {
localNickname: chatsModel.suggestionList.rowData(i, "localNickname")
})
chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
inputArea.chatInput.suggestionsList.append(suggestionsObj[suggestionsObj.length - 1]);
idMap[contactAddr] = true;
}
const len2 = chatsModel.messageList.rowCount();
@ -135,7 +139,7 @@ StackLayout {
let message = chatsModel.messageList.getMessageData(replyMessageIndex, "message")
let identicon = chatsModel.messageList.getMessageData(replyMessageIndex, "identicon")
chatInput.showReplyArea(userName, message, identicon)
inputArea.chatInput.showReplyArea(userName, message, identicon)
}
function requestAddressForTransaction(address, amount, tokenAddress, tokenDecimals = 18) {
@ -165,6 +169,25 @@ StackLayout {
}
}
function clickOnNotification() {
applicationWindow.show()
applicationWindow.raise()
applicationWindow.requestActivate()
appMain.changeAppSection(Constants.chat)
if (currentNotificationChatId) {
chatsModel.setActiveChannel(currentNotificationChatId)
} else if (currentNotificationCommunityId) {
chatsModel.communities.setActiveCommunity(currentNotificationCommunityId)
}
}
Connections {
target: systemTray
onMessageClicked: function () {
clickOnNotification()
}
}
Timer {
id: timer
}
@ -222,54 +245,9 @@ StackLayout {
}
}
Item {
visible: chatsModel.activeChannel.chatType === Constants.chatTypeOneToOne &&
!profileModel.contacts.isAdded(activeChatId) &&
!doNotShowAddToContactBannerToThose.includes(activeChatId)
AddToContactBanner {
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
height: 36
SVGImage {
source: "../../img/plusSign.svg"
anchors.right: addToContactsTxt.left
anchors.rightMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: ColorOverlay { color: addToContactsTxt.color }
}
StyledText {
id: addToContactsTxt
text: qsTr("Add to contacts")
color: Style.current.primary
anchors.centerIn: parent
}
Separator {
anchors.bottom: parent.bottom
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: profileModel.contacts.addContact(activeChatId)
}
StatusIconButton {
id: closeBtn
icon.name: "close"
onClicked: {
const newArray = Object.assign([], doNotShowAddToContactBannerToThose)
newArray.push(activeChatId)
doNotShowAddToContactBannerToThose = newArray
}
width: 20
height: 20
anchors.right: parent.right
anchors.rightMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
}
}
StackLayout {
@ -306,8 +284,8 @@ StackLayout {
Connections {
target: chatsModel
onActiveChannelChanged: {
chatInput.suggestions.hide();
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
inputArea.chatInput.suggestions.hide();
inputArea.chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
populateSuggestions();
}
onMessagePushed: {
@ -322,97 +300,17 @@ StackLayout {
}
}
Rectangle {
ChatRequestMessage {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
Layout.bottomMargin: Style.current.bigPadding
}
InputArea {
id: inputArea
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
Layout.preferredWidth: parent.width
height: chatInput.height
Layout.preferredHeight: height
color: "transparent"
Connections {
target: chatsModel
onLoadingMessagesChanged:
if(value){
loadingMessagesIndicator.active = true
} else {
timer.setTimeout(function(){
loadingMessagesIndicator.active = false;
}, 5000);
}
}
Loader {
id: loadingMessagesIndicator
active: chatsModel.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: {
const community = chatsModel.communities.activeCommunity
if (chatsModel.activeChannel.chatType === Constants.chatTypePrivateGroupChat) {
return chatsModel.activeChannel.isMember
}
return !community.active ||
community.access === Constants.communityChatPublicAccess ||
community.admin ||
chatsModel.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.activeChannel.chatType
onSendTransactionCommandButtonClicked: {
if (chatsModel.activeChannel.ensVerified) {
txModalLoader.sourceComponent = cmpSendTransactionWithEns
} else {
txModalLoader.sourceComponent = cmpSendTransactionNoEns
}
txModalLoader.item.open()
}
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.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;
}
}
}
}
}

View File

@ -0,0 +1,53 @@
import QtQuick 2.13
import QtGraphicalEffects 1.13
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
Item {
visible: chatsModel.activeChannel.chatType === Constants.chatTypeOneToOne &&
!isContact &&
!doNotShowAddToContactBannerToThose.includes(activeChatId)
height: 36
SVGImage {
source: "../../../../img/plusSign.svg"
anchors.right: addToContactsTxt.left
anchors.rightMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: ColorOverlay { color: addToContactsTxt.color }
}
StyledText {
id: addToContactsTxt
text: qsTr("Add to contacts")
color: Style.current.primary
anchors.centerIn: parent
}
Separator {
anchors.bottom: parent.bottom
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: profileModel.contacts.addContact(activeChatId)
}
StatusIconButton {
id: closeBtn
icon.name: "close"
onClicked: {
const newArray = Object.assign([], doNotShowAddToContactBannerToThose)
newArray.push(activeChatId)
doNotShowAddToContactBannerToThose = newArray
}
width: 20
height: 20
anchors.right: parent.right
anchors.rightMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
}
}

View File

@ -0,0 +1,48 @@
import QtQuick 2.13
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
Item {
visible: chatsModel.activeChannel.chatType === Constants.chatTypeOneToOne && !isContact
width: parent.width
height: childrenRect.height
Image {
id: waveImg
source: "../../../../img/wave.png"
width: 80
height: 80
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
id: contactText1
text: qsTr("You need to be mutual contacts with this person for them to receive your messages")
anchors.top: waveImg.bottom
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
anchors.topMargin: Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width / 1.3
}
StyledText {
id: contactText2
text: qsTr("Just click this button to add them as contact. They will receive a notification all once they accept you as contact as well, you'll be able to chat")
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
anchors.top: contactText1.bottom
anchors.topMargin: 2
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width / 1.3
}
StatusButton {
text: qsTr("Add to contacts")
anchors.top: contactText2.bottom
anchors.topMargin: Style.current.smallPadding
anchors.horizontalCenter: parent.horizontalCenter
onClicked: profileModel.contacts.addContact(activeChatId)
}
}

View File

@ -0,0 +1,95 @@
import QtQuick 2.13
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
import "../../components"
Item {
property alias chatInput: chatInput
id: inputArea
height: chatInput.height
Connections {
target: chatsModel
onLoadingMessagesChanged:
if(value){
loadingMessagesIndicator.active = true
} else {
timer.setTimeout(function(){
loadingMessagesIndicator.active = false;
}, 5000);
}
}
Loader {
id: loadingMessagesIndicator
active: chatsModel.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: {
const community = chatsModel.communities.activeCommunity
if (chatsModel.activeChannel.chatType === Constants.chatTypePrivateGroupChat) {
return chatsModel.activeChannel.isMember
}
return !community.active ||
community.access === Constants.communityChatPublicAccess ||
community.admin ||
chatsModel.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.activeChannel.chatType
onSendTransactionCommandButtonClicked: {
if (chatsModel.activeChannel.ensVerified) {
txModalLoader.sourceComponent = cmpSendTransactionWithEns
} else {
txModalLoader.sourceComponent = cmpSendTransactionNoEns
}
txModalLoader.item.open()
}
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.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;
}
}
}
}

View File

@ -7,6 +7,7 @@ import QtQml.Models 2.13
import QtGraphicalEffects 1.13
import QtQuick.Dialogs 1.3
import "../../../../shared"
import "../../../../shared/status"
import "../../../../imports"
import "../components"
import "./samples/"
@ -31,8 +32,6 @@ ScrollView {
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
property string currentNotificationChatId
id: chatLogView
anchors.fill: parent
anchors.bottomMargin: Style.current.bigPadding
@ -47,23 +46,33 @@ ScrollView {
verticalLayoutDirection: ListView.BottomToTop
// This header and Connections is to create an invisible padding so that the chat identifier is at the top
// The Connections is necessary, because doing the check inside teh ehader created a binding loop (the contentHeight includes the header height
// The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height
// If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections
header: Item {
height: 0
width: chatLogView.width
}
function checkHeaderHeight() {
if (!chatLogView.headerItem) {
return
}
if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
}
}
Connections {
id: contentHeightConnection
enabled: true
target: chatLogView
onContentHeightChanged: {
if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
contentHeightConnection.enabled = false
}
chatLogView.checkHeaderHeight()
}
onHeightChanged: {
chatLogView.checkHeaderHeight()
}
}
@ -148,14 +157,6 @@ ScrollView {
return true
}
function clickOnNotification(chatId) {
applicationWindow.show()
applicationWindow.raise()
applicationWindow.requestActivate()
chatsModel.setActiveChannel(chatId)
appMain.changeAppSection(Constants.chat)
}
Connections {
target: chatsModel
onMessagesLoaded: {
@ -191,7 +192,8 @@ ScrollView {
return
}
chatLogView.currentNotificationChatId = chatId
chatColumnLayout.currentNotificationChatId = chatId
chatColumnLayout.currentNotificationCommunityId = null
let name;
if (appSettings.notificationMessagePreviewSetting === Constants.notificationPreviewAnonymous) {
@ -223,7 +225,7 @@ ScrollView {
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
} else {
notificationWindow.notifyUser(chatId, name, message, chatType, identicon, chatLogView.clickOnNotification)
notificationWindow.notifyUser(chatId, name, message, chatType, identicon, chatColumnLayout.clickOnNotification)
}
}
}
@ -232,7 +234,9 @@ ScrollView {
Connections {
target: chatsModel.communities
onMembershipRequestChanged: function (communityName, accepted) {
onMembershipRequestChanged: function (communityId, communityName, accepted) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage("Status",
accepted ? qsTr("You have been accepted into the %1 community").arg(communityName) :
qsTr("Your request to join the %1 community was declined").arg(communityName),
@ -240,7 +244,9 @@ ScrollView {
Constants.notificationPopupTTL)
}
onMembershipRequestPushed: function (communityName, pubKey) {
onMembershipRequestPushed: function (communityId, communityName, pubKey) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage(qsTr("New membership request"),
qsTr("%1 asks to join %2").arg(Utils.getDisplayName(pubKey)).arg(communityName),
SystemTrayIcon.NoIcon,
@ -248,13 +254,6 @@ ScrollView {
}
}
Connections {
target: systemTray
onMessageClicked: {
chatLogView.clickOnNotification(chatLogView.currentNotificationChatId)
}
}
property var loadMsgs : Backpressure.oneInTime(chatLogView, 500, function() {
if(loadingMessages) return;
loadingMessages = true;

View File

@ -1,9 +1,11 @@
import QtQuick 2.13
import Qt.labs.platform 1.1
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../imports"
import "../../../shared"
import "../../../shared/status"
import "./components"
import "./ContactsColumn"
import "./CommunityComponents"
@ -90,6 +92,15 @@ Rectangle {
}
}
Component {
id: contactRequestsPopup
ContactRequestsPopup {
onClosed: {
destroy()
}
}
}
SearchBox {
id: searchBox
anchors.top: title.bottom
@ -108,9 +119,37 @@ Rectangle {
anchors.topMargin: Style.current.padding
}
Connections {
target: profileModel.contacts
onContactRequestAdded: {
systemTray.showMessage(qsTr("New contact request"),
qsTr("%1 requests to become contacts").arg(Utils.removeStatusEns(name)),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
}
}
StatusSettingsLineButton {
property int nbRequests: profileModel.contacts.contactRequests.count
id: contactRequest
anchors.top: searchBox.bottom
anchors.topMargin: visible ? Style.current.padding : 0
anchors.left: parent.left
anchors.leftMargin: Style.current.halfPadding
anchors.right: parent.right
anchors.rightMargin: Style.current.halfPadding
visible: nbRequests > 0
height: visible ? implicitHeight : 0
text: qsTr("Contact requests")
isBadge: true
badgeText: nbRequests.toString()
onClicked: openPopup(contactRequestsPopup)
}
ScrollView {
id: chatGroupsContainer
anchors.top: searchBox.bottom
anchors.top: contactRequest.bottom
anchors.topMargin: Style.current.padding
anchors.bottom: parent.bottom
anchors.left: parent.left

View File

@ -0,0 +1,106 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
import "../../Profile/Sections/Contacts"
ModalPopup {
id: popup
title: qsTr("Contact requests")
ListView {
id: contactList
property Component profilePopupComponent: ProfilePopup {
id: profilePopup
onClosed: destroy()
}
anchors.fill: parent
anchors.leftMargin: -Style.current.halfPadding
anchors.rightMargin: -Style.current.halfPadding
model: profileModel.contacts.contactRequests
clip: true
delegate: ContactRequest {
name: Utils.removeStatusEns(model.name)
address: model.address
localNickname: model.localNickname
identicon: model.thumbnailImage || model.identicon
profileClick: function (showFooter, userName, fromAuthor, identicon, textParam, nickName) {
var popup = profilePopupComponent.createObject(contactList);
popup.openPopup(showFooter, userName, fromAuthor, identicon, textParam, nickName);
}
onBlockContactActionTriggered: {
blockContactConfirmationDialog.contactName = name
blockContactConfirmationDialog.contactAddress = address
blockContactConfirmationDialog.open()
}
}
}
footer: Item {
width: parent.width
height: children[0].height
BlockContactConfirmationDialog {
id: blockContactConfirmationDialog
onBlockButtonClicked: {
profileModel.contacts.blockContact(blockContactConfirmationDialog.contactAddress)
blockContactConfirmationDialog.close()
}
}
ConfirmationDialog {
id: declineAllDialog
title: qsTr("Decline all contacts")
confirmationText: qsTr("Are you sure you want to decline all these contact requests")
onConfirmButtonClicked: {
const pubkeys = []
for (let i = 0; i < contactList.count; i++) {
pubkeys.push(contactList.itemAtIndex(i).address)
}
profileModel.contacts.rejectContactRequests(JSON.stringify(pubkeys))
declineAllDialog.close()
}
}
ConfirmationDialog {
id: acceptAllDialog
title: qsTr("Accept all contacts")
confirmationText: qsTr("Are you sure you want to accept all these contact requests")
onConfirmButtonClicked: {
const pubkeys = []
for (let i = 0; i < contactList.count; i++) {
pubkeys.push(contactList.itemAtIndex(i).address)
}
profileModel.contacts.acceptContactRequests(JSON.stringify(pubkeys))
acceptAllDialog.close()
}
}
StatusButton {
id: blockBtn
anchors.right: addToContactsButton.left
anchors.rightMargin: Style.current.padding
anchors.bottom: parent.bottom
type: "warn"
text: qsTr("Decline all")
onClicked: declineAllDialog.open()
}
StatusButton {
id: addToContactsButton
anchors.right: parent.right
text: qsTr("Accept all")
anchors.bottom: parent.bottom
onClicked: acceptAllDialog.open()
}
}
}

View File

@ -61,7 +61,7 @@ ListView {
// TODO: Make ConfirmationDialog a dynamic component on a future refactor
ConfirmationDialog {
id: removeContactConfirmationDialog
title: qsTrId("remove-contact")
title: qsTr("Remove contact")
//% "Are you sure you want to remove this contact?"
confirmationText: qsTrId("are-you-sure-you-want-to-remove-this-contact-")
onConfirmButtonClicked: {

View File

@ -0,0 +1,130 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
Rectangle {
property string name
property string address
property string identicon
property string localNickname
property var profileClick: function() {}
signal blockContactActionTriggered(name: string, address: string)
property bool isHovered: false
id: container
height: visible ? 64 : 0
anchors.right: parent.right
anchors.left: parent.left
border.width: 0
radius: Style.current.radius
color: isHovered ? Style.current.backgroundHover : Style.current.transparent
StatusImageIdenticon {
id: accountImage
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
source: identicon
}
StyledText {
id: usernameText
text: name
elide: Text.ElideRight
font.pixelSize: 17
anchors.top: accountImage.top
anchors.topMargin: Style.current.smallPadding
anchors.left: accountImage.right
anchors.leftMargin: Style.current.padding
anchors.right: declineBtn.left
anchors.rightMargin: Style.current.padding
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton) {
contactContextMenu.popup()
return
}
}
}
HoverHandler {
onHoveredChanged: container.isHovered = hovered
}
StatusIconButton {
id: declineBtn
icon.name: "close"
onClicked: profileModel.contacts.rejectContactRequest(container.address)
width: 32
height: 32
padding: 6
iconColor: Style.current.danger
hoveredIconColor: Style.current.danger
highlightedBackgroundColor: Utils.setColorAlpha(Style.current.danger, 0.1)
anchors.right: acceptBtn.left
anchors.rightMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
}
StatusIconButton {
id: acceptBtn
icon.name: "check-circle"
onClicked: profileModel.contacts.addContact(container.address)
width: 32
height: 32
padding: 6
iconColor: Style.current.success
hoveredIconColor: Style.current.success
highlightedBackgroundColor: Utils.setColorAlpha(Style.current.success, 0.1)
anchors.right: menuButton.left
anchors.rightMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
}
StatusContextMenuButton {
property int iconSize: 14
id: menuButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
MouseArea {
id: mouseArea
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
contactContextMenu.popup()
}
PopupMenu {
id: contactContextMenu
hasArrow: false
Action {
icon.source: "../../../../img/profileActive.svg"
icon.width: menuButton.iconSize
icon.height: menuButton.iconSize
//% "View Profile"
text: qsTrId("view-profile")
onTriggered: profileClick(true, name, address, identicon, "", localNickname)
enabled: true
}
Separator {}
Action {
icon.source: "../../../../img/block-icon.svg"
icon.width: menuButton.iconSize
icon.height: menuButton.iconSize
icon.color: Style.current.danger
text: qsTr("Decline and block")
onTriggered: container.blockContactActionTriggered(name, address)
}
}
}
}
}

BIN
ui/app/img/wave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -96,10 +96,13 @@ DISTFILES += \
app/AppLayouts/Browser/FavoritesBar.qml \
app/AppLayouts/Browser/FavoritesList.qml \
app/AppLayouts/Browser/components/BookmarkButton.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/AddToContactBanner.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatCommandButton.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatCommandModal.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatCommandsPopup.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatInputButton.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/ChatRequestMessage.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/InputArea.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/RequestModal.qml \
app/AppLayouts/Chat/ChatColumn/ChatComponents/SignTransactionModal.qml \
app/AppLayouts/Chat/ChatColumn/CompactMessage.qml \
@ -156,6 +159,7 @@ DISTFILES += \
app/AppLayouts/Chat/components/CommunitiesPopup.qml \
app/AppLayouts/Chat/components/CommunityDetailPopup.qml \
app/AppLayouts/Chat/components/ContactList.qml \
app/AppLayouts/Chat/components/ContactRequestsPopup.qml \
app/AppLayouts/Chat/components/CreateCommunityPopup.qml \
app/AppLayouts/Chat/components/EmojiCategoryButton.qml \
app/AppLayouts/Chat/components/EmojiPopup.qml \
@ -175,6 +179,7 @@ DISTFILES += \
app/AppLayouts/Profile/LeftTab/components/MenuButton.qml \
app/AppLayouts/Chat/data/EmojiReactions.qml \
app/AppLayouts/Profile/Sections/AppearanceContainer.qml \
app/AppLayouts/Profile/Sections/Contacts/ContactRequest.qml \
app/AppLayouts/Profile/Sections/NetworksModal.qml \
app/AppLayouts/Profile/Sections/BackupSeedModal.qml \
app/AppLayouts/Profile/Sections/BrowserContainer.qml \

View File

@ -119,19 +119,22 @@ Item {
text: {
switch(root.realChatType){
//% "Public chat"
case Constants.chatTypePublic: return qsTrId("public-chat")
case Constants.chatTypeOneToOne: return (profileModel.contacts.isAdded(root.chatId) ?
//% "Contact"
qsTrId("chat-is-a-contact") :
//% "Not a contact"
qsTrId("chat-is-not-a-contact"))
case Constants.chatTypePrivateGroupChat:
let cnt = chatsModel.activeChannel.members.rowCount();
//% "%1 members"
if(cnt > 1) return qsTrId("%1-members").arg(cnt);
//% "1 member"
return qsTrId("1-member");
default: return "...";
case Constants.chatTypePublic: return qsTrId("public-chat")
case Constants.chatTypeOneToOne: return (profileModel.contacts.isAdded(root.chatId) ?
profileModel.contacts.contactRequestReceived(root.chatId) ?
//% "Contact"
qsTrId("chat-is-a-contact") :
qsTr("Contact request pending") :
//% "Not a contact"
qsTrId("chat-is-not-a-contact"))
case Constants.chatTypePrivateGroupChat:
let cnt = chatsModel.activeChannel.members.rowCount();
//% "%1 members"
if(cnt > 1) return qsTrId("%1-members").arg(cnt);
//% "1 member"
return qsTrId("1-member");
default: return "...";
}
}
font.pixelSize: 12