feat(profile): implement contact management

This introduces the ability to:

- list search existing contacts
- block contacts
- unblock contacts
- list blocked contacts
- remove contacts
- search and add contacts

Closes #608
This commit is contained in:
Pascal Precht 2020-07-24 13:27:26 +02:00 committed by Iuri Matias
parent 9604faff08
commit e18188514a
16 changed files with 490 additions and 168 deletions

View File

@ -1,4 +1,4 @@
import NimQml, json, eventemitter, strutils import NimQml, json, eventemitter, strutils, sugar, sequtils
import json_serialization import json_serialization
import ../../status/libstatus/mailservers as status_mailservers import ../../status/libstatus/mailservers as status_mailservers
import ../../signals/types import ../../signals/types
@ -62,6 +62,14 @@ proc init*(self: ProfileController, account: Account) =
let contacts = self.status.contacts.getContacts() let contacts = self.status.contacts.getContacts()
self.view.setContactList(contacts) self.view.setContactList(contacts)
self.status.events.on("contactBlocked") do(e: Args):
let contacts = self.status.contacts.getContacts()
self.view.setContactList(contacts)
self.status.events.on("contactUnblocked") do(e: Args):
let contacts = self.status.contacts.getContacts()
self.view.setContactList(contacts)
self.status.events.on("contactRemoved") do(e: Args): self.status.events.on("contactRemoved") do(e: Args):
let contacts = self.status.contacts.getContacts() let contacts = self.status.contacts.getContacts()
self.view.setContactList(contacts) self.view.setContactList(contacts)

View File

@ -1,4 +1,4 @@
import NimQml, sequtils import NimQml, sequtils, strutils, sugar
import views/[mailservers_list, contact_list, profile_info, device_list] import views/[mailservers_list, contact_list, profile_info, device_list]
import ../../status/profile/[mailserver, profile, devices] import ../../status/profile/[mailserver, profile, devices]
import ../../status/profile as status_profile import ../../status/profile as status_profile
@ -6,6 +6,7 @@ import ../../status/contacts as status_contacts
import ../../status/accounts as status_accounts import ../../status/accounts as status_accounts
import ../../status/status import ../../status/status
import ../../status/devices as status_devices import ../../status/devices as status_devices
import ../../status/ens as status_ens
import ../../status/chat/chat import ../../status/chat/chat
import ../../status/libstatus/types import ../../status/libstatus/types
import qrcode/qrcode import qrcode/qrcode
@ -15,12 +16,15 @@ QtObject:
profile*: ProfileInfoView profile*: ProfileInfoView
mailserversList*: MailServersList mailserversList*: MailServersList
contactList*: ContactList contactList*: ContactList
addedContacts*: ContactList
blockedContacts*: ContactList
deviceList*: DeviceList deviceList*: DeviceList
mnemonic: string mnemonic: string
network: string network: string
status*: Status status*: Status
isDeviceSetup: bool isDeviceSetup: bool
changeLanguage*: proc(locale: string) changeLanguage*: proc(locale: string)
contactToAdd*: Profile
proc setup(self: ProfileView) = proc setup(self: ProfileView) =
self.QObject.setup self.QObject.setup
@ -28,6 +32,8 @@ QtObject:
proc delete*(self: ProfileView) = proc delete*(self: ProfileView) =
if not self.mailserversList.isNil: self.mailserversList.delete if not self.mailserversList.isNil: self.mailserversList.delete
if not self.contactList.isNil: self.contactList.delete if not self.contactList.isNil: self.contactList.delete
if not self.addedContacts.isNil: self.addedContacts.delete
if not self.blockedContacts.isNil: self.blockedContacts.delete
if not self.deviceList.isNil: self.deviceList.delete if not self.deviceList.isNil: self.deviceList.delete
if not self.profile.isNil: self.profile.delete if not self.profile.isNil: self.profile.delete
self.QObject.delete self.QObject.delete
@ -38,12 +44,19 @@ QtObject:
result.profile = newProfileInfoView() result.profile = newProfileInfoView()
result.mailserversList = newMailServersList() result.mailserversList = newMailServersList()
result.contactList = newContactList() result.contactList = newContactList()
result.addedContacts = newContactList()
result.blockedContacts = newContactList()
result.deviceList = newDeviceList() result.deviceList = newDeviceList()
result.mnemonic = "" result.mnemonic = ""
result.network = "" result.network = ""
result.status = status result.status = status
result.isDeviceSetup = false result.isDeviceSetup = false
result.changeLanguage = changeLanguage result.changeLanguage = changeLanguage
result.contactToAdd = Profile(
username: "",
alias: "",
ensName: ""
)
result.setup result.setup
proc addMailServerToList*(self: ProfileView, mailserver: MailServer) = proc addMailServerToList*(self: ProfileView, mailserver: MailServer) =
@ -58,6 +71,10 @@ QtObject:
proc updateContactList*(self: ProfileView, contacts: seq[Profile]) = proc updateContactList*(self: ProfileView, contacts: seq[Profile]) =
for contact in contacts: for contact in contacts:
self.contactList.updateContact(contact) self.contactList.updateContact(contact)
if contact.systemTags.contains(":contact/added"):
self.addedContacts.updateContact(contact)
if contact.systemTags.contains(":contact/blocked"):
self.blockedContacts.updateContact(contact)
proc contactListChanged*(self: ProfileView) {.signal.} proc contactListChanged*(self: ProfileView) {.signal.}
@ -66,6 +83,8 @@ QtObject:
proc setContactList*(self: ProfileView, contactList: seq[Profile]) = proc setContactList*(self: ProfileView, contactList: seq[Profile]) =
self.contactList.setNewData(contactList) 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.contactListChanged() self.contactListChanged()
QtProperty[QVariant] contactList: QtProperty[QVariant] contactList:
@ -73,6 +92,20 @@ QtObject:
write = setContactList write = setContactList
notify = contactListChanged notify = contactListChanged
proc getAddedContacts(self: ProfileView): QVariant {.slot.} =
return newQVariant(self.addedContacts)
QtProperty[QVariant] addedContacts:
read = getAddedContacts
notify = contactListChanged
proc getBlockedContacts(self: ProfileView): QVariant {.slot.} =
return newQVariant(self.blockedContacts)
QtProperty[QVariant] blockedContacts:
read = getBlockedContacts
notify = contactListChanged
proc mnemonicChanged*(self: ProfileView) {.signal.} proc mnemonicChanged*(self: ProfileView) {.signal.}
proc getMnemonic*(self: ProfileView): QVariant {.slot.} = proc getMnemonic*(self: ProfileView): QVariant {.slot.} =
@ -116,6 +149,27 @@ QtObject:
QtProperty[QVariant] profile: QtProperty[QVariant] profile:
read = getProfile read = getProfile
proc contactToAddChanged*(self: ProfileView) {.signal.}
proc getContactToAddUsername(self: ProfileView): QVariant {.slot.} =
var username = self.contactToAdd.alias;
if self.contactToAdd.ensVerified and self.contactToAdd.ensName != "":
username = self.contactToAdd.ensName
return newQVariant(username)
QtProperty[QVariant] contactToAddUsername:
read = getContactToAddUsername
notify = contactToAddChanged
proc getContactToAddPubKey(self: ProfileView): QVariant {.slot.} =
return newQVariant(self.contactToAdd.address)
QtProperty[QVariant] contactToAddPubKey:
read = getContactToAddPubKey
notify = contactToAddChanged
proc logout*(self: ProfileView) {.slot.} = proc logout*(self: ProfileView) {.slot.} =
self.status.profile.logout() self.status.profile.logout()
@ -174,3 +228,37 @@ QtObject:
status_devices.enable(installationId) status_devices.enable(installationId)
else: else:
status_devices.disable(installationId) status_devices.disable(installationId)
proc lookupContact*(self: ProfileView, value: string) {.slot.} =
if value == "":
return
var id = value
if not id.startsWith("0x"):
id = status_ens.pubkey(id)
let contact = self.status.contacts.getContactByID(id)
if contact != nil:
self.contactToAdd = contact
else:
self.contactToAdd = Profile(
username: "",
alias: "",
ensName: "",
ensVerified: false
)
self.contactToAddChanged()
proc addContact*(self: ProfileView, pk: string) {.slot.} =
discard self.status.contacts.addContact(pk)
proc unblockContact*(self: ProfileView, id: string) {.slot.} =
discard self.status.contacts.unblockContact(id)
proc blockContact*(self: ProfileView, id: string) {.slot.} =
discard self.status.contacts.blockContact(id)
proc removeContact*(self: ProfileView, id: string) {.slot.} =
self.status.contacts.removeContact(id)

View File

@ -10,6 +10,7 @@ type
Address = UserRole + 3 Address = UserRole + 3
Identicon = UserRole + 4 Identicon = UserRole + 4
IsContact = UserRole + 5 IsContact = UserRole + 5
IsBlocked = UserRole + 6
QtObject: QtObject:
type ContactList* = ref object of QAbstractListModel type ContactList* = ref object of QAbstractListModel
@ -46,6 +47,7 @@ QtObject:
of "identicon": result = contact.identicon of "identicon": result = contact.identicon
of "pubKey": result = contact.id of "pubKey": result = contact.id
of "isContact": result = $contact.isContact() of "isContact": result = $contact.isContact()
of "isBlocked": result = $contact.isBlocked()
method data(self: ContactList, index: QModelIndex, role: int): QVariant = method data(self: ContactList, index: QModelIndex, role: int): QVariant =
if not index.isValid: if not index.isValid:
@ -59,6 +61,7 @@ QtObject:
of ContactRoles.Identicon: result = newQVariant(contact.identicon) of ContactRoles.Identicon: result = newQVariant(contact.identicon)
of ContactRoles.PubKey: result = newQVariant(contact.id) of ContactRoles.PubKey: result = newQVariant(contact.id)
of ContactRoles.IsContact: result = newQVariant(contact.isContact()) of ContactRoles.IsContact: result = newQVariant(contact.isContact())
of ContactRoles.IsBlocked: result = newQVariant(contact.isBlocked())
method roleNames(self: ContactList): Table[int, string] = method roleNames(self: ContactList): Table[int, string] =
{ {
@ -66,7 +69,8 @@ QtObject:
ContactRoles.Address.int:"address", ContactRoles.Address.int:"address",
ContactRoles.Identicon.int:"identicon", ContactRoles.Identicon.int:"identicon",
ContactRoles.PubKey.int:"pubKey", ContactRoles.PubKey.int:"pubKey",
ContactRoles.IsContact.int:"isContact" ContactRoles.IsContact.int:"isContact",
ContactRoles.IsBlocked.int:"isBlocked"
}.toTable }.toTable
proc addContactToList*(self: ContactList, contact: Profile) = proc addContactToList*(self: ContactList, contact: Profile) =

View File

@ -26,7 +26,14 @@ proc getContactByID*(self: ContactModel, id: string): Profile =
proc blockContact*(self: ContactModel, id: string): string = proc blockContact*(self: ContactModel, id: string): string =
var contact = self.getContactByID(id) var contact = self.getContactByID(id)
contact.systemTags.add(":contact/blocked") contact.systemTags.add(":contact/blocked")
status_contacts.blockContact(contact) 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"))
discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.ensVerifiedAt, contact.ensVerificationRetries, contact.alias, contact.identicon, contact.systemTags)
self.events.emit("contactUnblocked", Args())
proc getContacts*(self: ContactModel): seq[Profile] = proc getContacts*(self: ContactModel): seq[Profile] =
result = map(status_contacts.getContacts().getElems(), proc(x: JsonNode): Profile = x.toProfileModel()) result = map(status_contacts.getContacts().getElems(), proc(x: JsonNode): Profile = x.toProfileModel())

View File

@ -32,9 +32,9 @@ SplitView {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 0 anchors.bottomMargin: 0
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 0 anchors.rightMargin: 113
anchors.left: leftTab.right anchors.left: leftTab.right
anchors.leftMargin: 0 anchors.leftMargin: 113
currentIndex: leftTab.currentTab currentIndex: leftTab.currentTab
// This list needs to match LeftTab/constants.js // This list needs to match LeftTab/constants.js

View File

@ -11,8 +11,11 @@ Rectangle {
property bool selectable: false property bool selectable: false
property var profileClick: function() {} property var profileClick: function() {}
property bool isContact: true property bool isContact: true
property bool isBlocked: false
property string searchStr: ""
id: container
visible: isContact visible: isContact && (searchStr == "" || name.includes(searchStr))
height: visible ? 64 : 0 height: visible ? 64 : 0
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
@ -26,6 +29,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
source: identicon source: identicon
} }
StyledText { StyledText {
id: usernameText id: usernameText
text: name text: name
@ -34,22 +38,106 @@ Rectangle {
anchors.rightMargin: Style.current.padding anchors.rightMargin: Style.current.padding
font.pixelSize: 17 font.pixelSize: 17
anchors.top: accountImage.top anchors.top: accountImage.top
anchors.topMargin: Style.current.smallPadding
anchors.left: accountImage.right anchors.left: accountImage.right
anchors.leftMargin: Style.current.padding anchors.leftMargin: Style.current.padding
} }
StyledText {
id: addressText Rectangle {
width: 108 property int iconSize: 14
font.family: Style.current.fontHexRegular.name id: menuButton
text: address height: 32
elide: Text.ElideMiddle width: 32
anchors.bottom: accountImage.bottom anchors.top: usernameText.top
anchors.bottomMargin: 0 anchors.verticalCenter: parent.verticalCenter
anchors.left: usernameText.left anchors.right: parent.right
anchors.leftMargin: 0 radius: 8
font.pixelSize: 15
color: Style.current.darkGrey SVGImage {
source: "../../../../img/dots-icon.svg"
width: 18
height: 4
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
} }
MouseArea {
id: mouseArea
property bool menuOpened: false
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
hoverEnabled: true
onExited: {
menuButton.color = Style.current.white
}
onEntered: {
menuButton.color = Style.current.grey
}
onClicked: {
menuOpened = true
contactContextMenu.popup()
}
PopupMenu {
id: contactContextMenu
hasArrow: false
onClosed: {
mouseArea.menuOpened = false
}
Action {
icon.source: "../../../../img/profileActive.svg"
icon.width: menuButton.iconSize
icon.height: menuButton.iconSize
text: qsTrId("view-profile")
onTriggered: profileClick(name, address, identicon)
enabled: true
}
Action {
icon.source: "../../../../img/message.svg"
icon.width: menuButton.iconSize
icon.height: menuButton.iconSize
text: qsTrId("send-message")
onTriggered: {
tabBar.currentIndex = 0
chatsModel.joinChat(address, Constants.chatTypeOneToOne)
}
enabled: !container.isBlocked
}
Action {
icon.source: "../../../../img/block-icon.svg"
icon.width: menuButton.iconSize
icon.height: menuButton.iconSize
text: qsTrId("block-user")
enabled: !container.isBlocked
onTriggered: {
profileModel.blockContact(address)
}
}
Action {
icon.source: "../../../../img/remove-contact.svg"
icon.width: menuButton.iconSize
icon.height: menuButton.iconSize
icon.color: Style.current.red
text: qsTrId("remove-contact")
enabled: container.isContact
onTriggered: profileModel.removeContact(address)
}
Action {
icon.source: "../../../../img/block-icon.svg"
icon.width: menuButton.iconSize
icon.height: menuButton.iconSize
icon.color: Style.current.red
text: qsTrId("unblock-user")
enabled: container.isBlocked
onTriggered: {
profileModel.unblockContact(address)
contactContextMenu.close()
}
}
}
}
}
RadioButton { RadioButton {
visible: selectable visible: selectable
anchors.top: parent.top anchors.top: parent.top
@ -57,12 +145,4 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
ButtonGroup.group: contactGroup ButtonGroup.group: contactGroup
} }
MouseArea {
enabled: !selectable
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
profileClick(name, address, identicon)
}
}
} }

View File

@ -11,6 +11,7 @@ ListView {
id: contactList id: contactList
property var contacts: ContactsData {} property var contacts: ContactsData {}
property var selectable: true property var selectable: true
property string searchStr: ""
property alias selectedContact: contactGroup.checkedButton property alias selectedContact: contactGroup.checkedButton
property string searchString: "" property string searchString: ""
property string lowerCaseSearchString: searchString.toLowerCase() property string lowerCaseSearchString: searchString.toLowerCase()
@ -23,6 +24,7 @@ ListView {
address: model.address address: model.address
identicon: model.identicon identicon: model.identicon
isContact: model.isContact isContact: model.isContact
isBlocked: model.isBlocked
selectable: contactList.selectable selectable: contactList.selectable
profileClick: profilePopup.openPopup.bind(profilePopup) profileClick: profilePopup.openPopup.bind(profilePopup)
visible: searchString === "" || visible: searchString === "" ||
@ -37,6 +39,7 @@ ListView {
ButtonGroup { ButtonGroup {
id: contactGroup id: contactGroup
} }
} }
/*##^## /*##^##
Designer { Designer {

View File

@ -9,33 +9,30 @@ import "./Contacts"
Item { Item {
id: contactsContainer id: contactsContainer
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true property alias searchStr: searchBox.text
Item {
anchors.top: parent.top
anchors.topMargin: 32
anchors.right: parent.right
anchors.rightMargin: contentMargin
anchors.left: parent.left
anchors.leftMargin: contentMargin
anchors.bottom: parent.bottom
SearchBox { SearchBox {
id: searchBox id: searchBox
anchors.top: parent.top
anchors.topMargin: 32
fontPixelSize: 15
} }
Item { Item {
id: addNewContact id: addNewContact
anchors.top: searchBox.bottom anchors.top: searchBox.bottom
anchors.topMargin: Style.current.bigPadding anchors.topMargin: Style.current.bigPadding
width: parent.width width: addButton.width + usernameText.width + Style.current.padding
height: addButton.height height: addButton.height
AddButton { AddButton {
id: addButton id: addButton
clickable: false clickable: false
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
} }
StyledText { StyledText {
id: usernameText id: usernameText
text: qsTr("Add new contact") text: qsTr("Add new contact")
@ -50,18 +47,122 @@ Item {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
// TODO implement adding a contact addContactModal.open()
console.log('Add a contact') }
}
}
Item {
id: blockedContactsButton
anchors.top: addNewContact.bottom
anchors.topMargin: Style.current.bigPadding
width: blockButton.width + blockButtonLabel.width + Style.current.padding
height: addButton.height
IconButton {
id: blockButton
clickable: false
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
iconName: "block-icon"
color: Style.current.lightBlue
}
StyledText {
id: blockButtonLabel
text: qsTr("Blocked contacts")
color: Style.current.blue
anchors.left: blockButton.right
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: blockButton.verticalCenter
font.pixelSize: 15
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
blockedContactsModal.open()
}
}
}
ModalPopup {
id: blockedContactsModal
title: qsTr("Blocked contacts")
ContactList {
anchors.top: parent.top
anchors.bottom: parent.bottom
contacts: profileModel.blockedContacts
selectable: false
}
}
ModalPopup {
id: addContactModal
title: qsTr("Add contact")
Input {
id: addContactSearchInput
placeholderText: qsTrId("Enter ENS username or chat key")
customHeight: 44
fontPixelSize: 15
onEditingFinished: {
profileModel.lookupContact(inputValue)
}
}
Item {
id: contactToAddInfo
anchors.top: addContactSearchInput.bottom
anchors.topMargin: Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
height: contactUsername.height
width: contactUsername.width + contactPubKey.width
visible: profileModel.contactToAddPubKey !== ""
StyledText {
id: contactUsername
text: profileModel.contactToAddUsername + " • "
anchors.top: addContactSearchInput.bottom
anchors.topMargin: Style.current.padding
font.pixelSize: 12
color: Style.current.darkGrey
}
StyledText {
id: contactPubKey
text: profileModel.contactToAddPubKey
anchors.left: contactUsername.right
width: 100
font.pixelSize: 12
elide: Text.ElideMiddle
color: Style.current.darkGrey
}
}
footer: StyledButton {
anchors.right: parent.right
anchors.leftMargin: Style.current.padding
//% "Send Message"
label: qsTr("Add contact")
disabled: !contactToAddInfo.visible
anchors.bottom: parent.bottom
onClicked: {
profileModel.addContact(profileModel.contactToAddPubKey);
addContactModal.close()
} }
} }
} }
ContactList { ContactList {
id: contactListView id: contactListView
anchors.top: addNewContact.bottom anchors.top: blockedContactsButton.bottom
anchors.topMargin: Style.current.bigPadding anchors.topMargin: Style.current.bigPadding
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
contacts: profileModel.contactList contacts: profileModel.addedContacts
selectable: false selectable: false
searchString: searchBox.text searchString: searchBox.text
} }
@ -96,7 +197,6 @@ Item {
} }
} }
} }
}
/*##^## /*##^##
Designer { Designer {

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.99967 13.6668C3.31778 13.6668 0.333008 10.6821 0.333008 7.00016C0.333008 3.31826 3.31778 0.333496 6.99967 0.333496C10.6816 0.333496 13.6663 3.31826 13.6663 7.00016C13.6663 10.6821 10.6816 13.6668 6.99967 13.6668ZM2.91208 10.3806C2.77375 10.519 2.54581 10.5094 2.42998 10.3518C1.74037 9.41312 1.33301 8.25421 1.33301 7.00016C1.33301 3.87055 3.87006 1.3335 6.99967 1.3335C8.25373 1.3335 9.41263 1.74086 10.3513 2.43047C10.509 2.5463 10.5185 2.77424 10.3802 2.91257L2.91208 10.3806ZM3.61919 11.0878C3.48086 11.2261 3.4904 11.454 3.64806 11.5699C4.58672 12.2595 5.74562 12.6668 6.99967 12.6668C10.1293 12.6668 12.6663 10.1298 12.6663 7.00016C12.6663 5.74611 12.259 4.5872 11.5694 3.64855C11.4535 3.49089 11.2256 3.48135 11.0873 3.61968L3.61919 11.0878Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

5
ui/app/img/dots-icon.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="18" height="4" viewBox="0 0 18 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2C4 3.10457 3.10457 4 2 4C0.895431 4 0 3.10457 0 2C0 0.895431 0.895431 0 2 0C3.10457 0 4 0.895431 4 2Z" fill="#939BA1"/>
<path d="M11 2C11 3.10457 10.1046 4 9 4C7.89543 4 7 3.10457 7 2C7 0.895431 7.89543 0 9 0C10.1046 0 11 0.895431 11 2Z" fill="#939BA1"/>
<path d="M16 4C17.1046 4 18 3.10457 18 2C18 0.895431 17.1046 0 16 0C14.8954 0 14 0.895431 14 2C14 3.10457 14.8954 4 16 4Z" fill="#939BA1"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@ -0,0 +1,5 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.00033 6.66667C7.84127 6.66667 9.33366 5.17428 9.33366 3.33333C9.33366 1.49238 7.84127 0 6.00033 0C4.15938 0 2.66699 1.49238 2.66699 3.33333C2.66699 5.17428 4.15938 6.66667 6.00033 6.66667ZM6.00033 5.66667C7.28899 5.66667 8.33366 4.622 8.33366 3.33333C8.33366 2.04467 7.28899 1 6.00033 1C4.71166 1 3.66699 2.04467 3.66699 3.33333C3.66699 4.622 4.71166 5.66667 6.00033 5.66667Z" fill="#FF2D55"/>
<path d="M6.62336 9.18929C6.60016 9.46766 6.35289 9.67067 6.07357 9.66713C6.04916 9.66682 6.02471 9.66667 6.00022 9.66667C4.30462 9.66667 2.78296 10.4114 1.74449 11.5916C1.54282 11.8208 1.18548 11.8519 0.969607 11.636C0.795787 11.4622 0.774868 11.1856 0.934774 10.9989C2.15749 9.57132 3.97321 8.66667 6.00022 8.66667C6.05818 8.66667 6.11597 8.66741 6.17357 8.66888C6.44624 8.67584 6.64601 8.91747 6.62336 9.18929Z" fill="#FF2D55"/>
<path d="M8.66699 8.66667C8.39085 8.66667 8.16699 8.89052 8.16699 9.16667C8.16699 9.44281 8.39085 9.66667 8.66699 9.66667H12.0003C12.2765 9.66667 12.5003 9.44281 12.5003 9.16667C12.5003 8.89052 12.2765 8.66667 12.0003 8.66667H8.66699Z" fill="#FF2D55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -308,6 +308,7 @@ DISTFILES += \
onboarding/img/wallet@3x.jpg \ onboarding/img/wallet@3x.jpg \
onboarding/qmldir \ onboarding/qmldir \
shared/AddButton.qml \ shared/AddButton.qml \
shared/IconButton.qml \
shared/Input.qml \ shared/Input.qml \
shared/ModalPopup.qml \ shared/ModalPopup.qml \
shared/NotificationWindow.qml \ shared/NotificationWindow.qml \

View File

@ -3,80 +3,9 @@ import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import Qt.labs.platform 1.1 import Qt.labs.platform 1.1
import "../imports" import "../imports"
import "./"
Rectangle { IconButton {
signal clicked id: iconButton
property int iconWidth: 14 iconName: "plusSign"
property int iconHeight: 14
property alias icon: imgIcon
property bool clickable: true
id: btnAddContainer
width: 36
height: 36
color: Style.current.blue
radius: width / 2
Image {
id: imgIcon
fillMode: Image.PreserveAspectFit
source: "../app/img/plusSign.svg"
width: iconWidth
height: iconHeight
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
state: "default"
rotation: 0
states: [
State {
name: "default"
PropertyChanges {
target: imgIcon
rotation: 0
}
},
State {
name: "rotated"
PropertyChanges {
target: imgIcon
rotation: 45
}
}
]
transitions: [
Transition {
from: "default"
to: "rotated"
RotationAnimation {
duration: 150
direction: RotationAnimation.Clockwise
easing.type: Easing.InCubic
}
},
Transition {
from: "rotated"
to: "default"
RotationAnimation {
duration: 150
direction: RotationAnimation.Counterclockwise
easing.type: Easing.OutCubic
}
}
]
}
MouseArea {
id: mouseArea
visible: btnAddContainer.clickable
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: {
imgIcon.state = "rotated"
btnAddContainer.clicked()
}
}
} }

83
ui/shared/IconButton.qml Normal file
View File

@ -0,0 +1,83 @@
import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import Qt.labs.platform 1.1
import "../imports"
Rectangle {
signal clicked
property int iconWidth: 14
property int iconHeight: 14
property alias icon: imgIcon
property bool clickable: true
property string iconName: "plusSign"
id: btnAddContainer
width: 36
height: 36
color: Style.current.blue
radius: width / 2
Image {
id: imgIcon
fillMode: Image.PreserveAspectFit
source: "../app/img/" + parent.iconName + ".svg"
width: iconWidth
height: iconHeight
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
state: "default"
rotation: 0
states: [
State {
name: "default"
PropertyChanges {
target: imgIcon
rotation: 0
}
},
State {
name: "rotated"
PropertyChanges {
target: imgIcon
rotation: 45
}
}
]
transitions: [
Transition {
from: "default"
to: "rotated"
RotationAnimation {
duration: 150
direction: RotationAnimation.Clockwise
easing.type: Easing.InCubic
}
},
Transition {
from: "rotated"
to: "default"
RotationAnimation {
duration: 150
direction: RotationAnimation.Counterclockwise
easing.type: Easing.OutCubic
}
}
]
}
MouseArea {
id: mouseArea
visible: btnAddContainer.clickable
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: {
imgIcon.state = "rotated"
btnAddContainer.clicked()
}
}
}

View File

@ -21,6 +21,7 @@ Item {
readonly property int labelMargin: 7 readonly property int labelMargin: 7
property int customHeight: 44 property int customHeight: 44
property int fontPixelSize: 15 property int fontPixelSize: 15
signal editingFinished(string inputValue)
id: inputBox id: inputBox
height: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0) + (!!validationError ? validationErrorText.height : 0) height: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0) + (!!validationError ? validationErrorText.height : 0)
@ -70,6 +71,7 @@ Item {
background: Rectangle { background: Rectangle {
color: Style.current.transparent color: Style.current.transparent
} }
onEditingFinished: inputBox.editingFinished(inputBox.text)
} }
SVGImage { SVGImage {

View File

@ -7,6 +7,7 @@ import "../shared"
Menu { Menu {
property alias arrowX: bgPopupMenuTopArrow.x property alias arrowX: bgPopupMenuTopArrow.x
property int paddingSize: 8 property int paddingSize: 8
property bool hasArrow: true
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnReleaseOutside | Popup.CloseOnEscape closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnReleaseOutside | Popup.CloseOnEscape
id: popupMenu id: popupMenu
topPadding: bgPopupMenuTopArrow.height + paddingSize topPadding: bgPopupMenuTopArrow.height + paddingSize
@ -18,6 +19,8 @@ Menu {
implicitHeight: 34 implicitHeight: 34
font.pixelSize: 13 font.pixelSize: 13
icon.color: popupMenuItem.action.icon.color != "#00000000" ? popupMenuItem.action.icon.color : Style.current.blue icon.color: popupMenuItem.action.icon.color != "#00000000" ? popupMenuItem.action.icon.color : Style.current.blue
visible: popupMenuItem.action.enabled
height: popupMenuItem.action.enabled ? popupMenuItem.implicitHeight : 0
contentItem: Item { contentItem: Item {
id: menuItemContent id: menuItemContent
@ -66,6 +69,7 @@ Menu {
color: "transparent" color: "transparent"
Rectangle { Rectangle {
id: bgPopupMenuTopArrow id: bgPopupMenuTopArrow
visible: popupMenu.hasArrow
color: Style.current.modalBackground color: Style.current.modalBackground
height: 14 height: 14
width: 14 width: 14