feat: add profile pic support

This commit is contained in:
Jonathan Rainville 2020-11-30 12:03:52 -05:00 committed by Iuri Matias
parent 43d9d5184d
commit d01c9fef79
48 changed files with 687 additions and 128 deletions

View File

@ -15,6 +15,8 @@ import web3/[conversions, ethtypes]
import ../../status/threads import ../../status/threads
import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions] import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions]
import json_serialization import json_serialization
import ../../status/libstatus/utils
import ../utils/image_utils
logScope: logScope:
topics = "chats-view" topics = "chats-view"
@ -149,10 +151,7 @@ QtObject:
proc sendImage*(self: ChatsView, imagePath: string): string {.slot.} = proc sendImage*(self: ChatsView, imagePath: string): string {.slot.} =
result = "" result = ""
try: try:
var image: string = replace(imagePath, "file://", "") var image = image_utils.formatImagePath(imagePath)
if defined(windows):
# Windows doesn't work with paths starting with a slash
image.removePrefix('/')
let tmpImagePath = image_resizer(image, 2000, TMPDIR) let tmpImagePath = image_resizer(image, 2000, TMPDIR)
self.status.chat.sendImage(self.activeChannel.id, tmpImagePath) self.status.chat.sendImage(self.activeChannel.id, tmpImagePath)
removeFile(tmpImagePath) removeFile(tmpImagePath)

View File

@ -1,4 +1,4 @@
import NimQml, Tables, json, nimcrypto, strformat, json_serialization import NimQml, Tables, json, nimcrypto, strformat, json_serialization, chronicles
import ../../status/signals/types import ../../status/signals/types
import ../../status/libstatus/types as status_types import ../../status/libstatus/types as status_types
import ../../status/libstatus/accounts as status_accounts import ../../status/libstatus/accounts as status_accounts
@ -9,9 +9,11 @@ import core
type type
AccountRoles {.pure.} = enum AccountRoles {.pure.} = enum
Username = UserRole + 1, Username = UserRole + 1
Identicon = UserRole + 2, Identicon = UserRole + 2
Address = UserRole + 3 Address = UserRole + 3
ThumbnailImage = UserRole + 4
LargeImage = UserRole + 5
QtObject: QtObject:
type LoginView* = ref object of QAbstractListModel type LoginView* = ref object of QAbstractListModel
@ -41,7 +43,12 @@ QtObject:
proc setCurrentAccount*(self: LoginView, selectedAccountIdx: int) {.slot.} = proc setCurrentAccount*(self: LoginView, selectedAccountIdx: int) {.slot.} =
let currNodeAcct = self.accounts[selectedAccountIdx] let currNodeAcct = self.accounts[selectedAccountIdx]
self.currentAccount.setAccount(GeneratedAccount(name: currNodeAcct.name, photoPath: currNodeAcct.photoPath, address: currNodeAcct.keyUid)) self.currentAccount.setAccount(GeneratedAccount(
name: currNodeAcct.name,
identicon: currNodeAcct.identicon,
address: currNodeAcct.keyUid,
identityImage: currNodeAcct.identityImage
))
QtProperty[QVariant] currentAccount: QtProperty[QVariant] currentAccount:
read = getCurrentAccount read = getCurrentAccount
@ -72,13 +79,25 @@ QtObject:
let assetRole = role.AccountRoles let assetRole = role.AccountRoles
case assetRole: case assetRole:
of AccountRoles.Username: result = newQVariant(asset.name) of AccountRoles.Username: result = newQVariant(asset.name)
of AccountRoles.Identicon: result = newQVariant(asset.photoPath) of AccountRoles.Identicon: result = newQVariant(asset.identicon)
of AccountRoles.Address: result = newQVariant(asset.keyUid) of AccountRoles.Address: result = newQVariant(asset.keyUid)
of AccountRoles.ThumbnailImage:
if (not asset.identityImage.isNil):
result = newQVariant(asset.identityImage.thumbnail)
else:
result = newQVariant(asset.identicon)
of AccountRoles.LargeImage:
if (not asset.identityImage.isNil):
result = newQVariant(asset.identityImage.large)
else:
result = newQVariant(asset.identicon)
method roleNames(self: LoginView): Table[int, string] = method roleNames(self: LoginView): Table[int, string] =
{ AccountRoles.Username.int:"username", { AccountRoles.Username.int:"username",
AccountRoles.Identicon.int:"identicon", AccountRoles.Identicon.int:"identicon",
AccountRoles.Address.int:"address" }.toTable AccountRoles.Address.int:"address",
AccountRoles.ThumbnailImage.int:"thumbnailImage",
AccountRoles.LargeImage.int:"largeImage" }.toTable
proc login(self: LoginView, password: string): string {.slot.} = proc login(self: LoginView, password: string): string {.slot.} =
var currentAccountId = 0 var currentAccountId = 0

View File

@ -57,7 +57,7 @@ QtObject:
let assetRole = role.AccountRoles let assetRole = role.AccountRoles
case assetRole: case assetRole:
of AccountRoles.Username: result = newQVariant(asset.name) of AccountRoles.Username: result = newQVariant(asset.name)
of AccountRoles.Identicon: result = newQVariant(asset.photoPath) of AccountRoles.Identicon: result = newQVariant(asset.identicon)
of AccountRoles.Address: result = newQVariant(asset.keyUid) of AccountRoles.Address: result = newQVariant(asset.keyUid)
method roleNames(self: OnboardingView): Table[int, string] = method roleNames(self: OnboardingView): Table[int, string] =

View File

@ -28,11 +28,29 @@ QtObject:
read = username read = username
notify = accountChanged notify = accountChanged
proc identicon*(self: AccountInfoView): string {.slot.} = result = ?.self.account.photoPath proc identicon*(self: AccountInfoView): string {.slot.} = result = ?.self.account.identicon
QtProperty[string] identicon: QtProperty[string] identicon:
read = identicon read = identicon
notify = accountChanged notify = accountChanged
proc thumbnailImage*(self: AccountInfoView): string {.slot.} =
if (not self.account.identityImage.isNil):
result = self.account.identityImage.thumbnail
else:
result = ?.self.account.identicon
QtProperty[string] thumbnailImage:
read = thumbnailImage
notify = identityImageChanged
proc largeImage*(self: AccountInfoView): string {.slot.} =
if (not self.account.identityImage.isNil):
result = self.account.identityImage.large
else:
result = ?.self.account.identicon
QtProperty[string] largeImage:
read = largeImage
notify = identityImageChanged
proc address*(self: AccountInfoView): string {.slot.} = result = ?.self.account.address proc address*(self: AccountInfoView): string {.slot.} = result = ?.self.account.address
QtProperty[string] address: QtProperty[string] address:
read = address read = address

View File

@ -43,6 +43,11 @@ proc init*(self: ProfileController, account: Account) =
profile.id = pubKey profile.id = pubKey
profile.address = account.keyUid profile.address = account.keyUid
let identityImage = self.status.profile.getIdentityImage(profile.address)
if (identityImage.thumbnail != ""):
profile.identityImage = identityImage
self.view.devices.addDevices(status_devices.getAllDevices()) self.view.devices.addDevices(status_devices.getAllDevices())
self.view.devices.setDeviceSetup(status_devices.isDeviceSetup()) self.view.devices.setDeviceSetup(status_devices.isDeviceSetup())
self.view.setNewProfile(profile) self.view.setNewProfile(profile)

View File

@ -1,4 +1,4 @@
import NimQml, sequtils, strutils, sugar, os, json import NimQml, sequtils, strutils, sugar, os, json, chronicles
import views/[mailservers_list, ens_manager, contacts, devices, mailservers, mnemonic, network, fleets, profile_info, device_list, dapp_list] import views/[mailservers_list, ens_manager, contacts, devices, mailservers, mnemonic, network, fleets, profile_info, device_list, dapp_list]
import ../chat/views/channels_list import ../chat/views/channels_list
import ../../status/profile/profile import ../../status/profile/profile
@ -13,6 +13,7 @@ import ../../status/threads
import ../../status/libstatus/types import ../../status/libstatus/types
import ../../status/libstatus/accounts/constants as accountConstants import ../../status/libstatus/accounts/constants as accountConstants
import qrcode/qrcode import qrcode/qrcode
import ../utils/image_utils
QtObject: QtObject:
type ProfileView* = ref object of QObject type ProfileView* = ref object of QObject
@ -209,3 +210,24 @@ QtObject:
QtProperty[QVariant] network: QtProperty[QVariant] network:
read = getNetwork read = getNetwork
proc uploadNewProfilePic*(self: ProfileView, imageUrl: string, aX: int, aY: int, bX: int, bY: int): string {.slot.} =
var image = image_utils.formatImagePath(imageUrl)
# FIXME the function to get the file size is messed up
# var size = image_getFileSize(image)
# TODO find a way to i18n this (maybe send just a code and then QML sets the right string)
# return "Max file size is 20MB"
try:
# TODO add crop tool for the image
let identityImage = self.status.profile.storeIdentityImage(self.profile.address, image, aX, aY, bX, bY)
self.profile.setIdentityImage(identityImage)
result = ""
except Exception as e:
error "Error storing identity image", msg=e.msg
result = "Error storing identity image: " & e.msg
proc deleteProfilePic*(self: ProfileView): string {.slot.} =
result = self.status.profile.deleteIdentityImage(self.profile.address)
if (result == ""):
self.profile.removeIdentityImage()

View File

@ -1,4 +1,4 @@
import NimQml import NimQml, chronicles
import Tables import Tables
import ../../../status/profile/profile import ../../../status/profile/profile
from ../../../status/ens import nil from ../../../status/ens import nil
@ -14,6 +14,8 @@ type
Alias = UserRole + 7 Alias = UserRole + 7
EnsVerified = UserRole + 8 EnsVerified = UserRole + 8
LocalNickname = UserRole + 9 LocalNickname = UserRole + 9
ThumbnailImage = UserRole + 10
LargeImage = UserRole + 11
QtObject: QtObject:
type ContactList* = ref object of QAbstractListModel type ContactList* = ref object of QAbstractListModel
@ -42,6 +44,14 @@ QtObject:
return ens.userNameOrAlias(contact) return ens.userNameOrAlias(contact)
return defaultValue return defaultValue
proc getContactIndexByPubkey(self: ContactList, pubkey: string): int {.slot.} =
var i = 0
for contact in self.contacts:
if (contact.id == pubkey):
return i
i = i + 1
return -1
proc rowData(self: ContactList, index: int, column: string): string {.slot.} = proc rowData(self: ContactList, index: int, column: string): string {.slot.} =
let contact = self.contacts[index] let contact = self.contacts[index]
case column: case column:
@ -54,6 +64,8 @@ QtObject:
of "alias": result = contact.alias of "alias": result = contact.alias
of "ensVerified": result = $contact.ensVerified of "ensVerified": result = $contact.ensVerified
of "localNickname": result = $contact.localNickname of "localNickname": result = $contact.localNickname
of "thumbnailImage": result = $contact.identityImage.thumbnail
of "largeImage": result = $contact.identityImage.large
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:
@ -71,6 +83,8 @@ QtObject:
of ContactRoles.Alias: result = newQVariant(contact.alias) of ContactRoles.Alias: result = newQVariant(contact.alias)
of ContactRoles.EnsVerified: result = newQVariant(contact.ensVerified) of ContactRoles.EnsVerified: result = newQVariant(contact.ensVerified)
of ContactRoles.LocalNickname: result = newQVariant(contact.localNickname) of ContactRoles.LocalNickname: result = newQVariant(contact.localNickname)
of ContactRoles.ThumbnailImage: result = newQVariant(contact.identityImage.thumbnail)
of ContactRoles.LargeImage: result = newQVariant(contact.identityImage.large)
method roleNames(self: ContactList): Table[int, string] = method roleNames(self: ContactList): Table[int, string] =
{ {
@ -82,7 +96,9 @@ QtObject:
ContactRoles.IsBlocked.int:"isBlocked", ContactRoles.IsBlocked.int:"isBlocked",
ContactRoles.Alias.int:"alias", ContactRoles.Alias.int:"alias",
ContactRoles.LocalNickname.int:"localNickname", ContactRoles.LocalNickname.int:"localNickname",
ContactRoles.EnsVerified.int:"ensVerified" ContactRoles.EnsVerified.int:"ensVerified",
ContactRoles.ThumbnailImage.int:"thumbnailImage",
ContactRoles.LargeImage.int:"largeImage"
}.toTable }.toTable
proc addContactToList*(self: ContactList, contact: Profile) = proc addContactToList*(self: ContactList, contact: Profile) =
@ -95,6 +111,8 @@ QtObject:
if(c.isContact()): return true if(c.isContact()): return true
return false return false
proc contactChanged*(self: ContactList, pubkey: string) {.signal.}
proc updateContact*(self: ContactList, contact: Profile) = proc updateContact*(self: ContactList, contact: Profile) =
var found = false var found = false
let topLeft = self.createIndex(0, 0, nil) let topLeft = self.createIndex(0, 0, nil)
@ -104,11 +122,13 @@ QtObject:
found = true found = true
c.ensName = contact.ensName c.ensName = contact.ensName
c.ensVerified = contact.ensVerified c.ensVerified = contact.ensVerified
c.identityImage = contact.identityImage
if not found: if not found:
self.addContactToList(contact) self.addContactToList(contact)
else: else:
self.dataChanged(topLeft, bottomRight, @[ContactRoles.Name.int]) self.dataChanged(topLeft, bottomRight, @[ContactRoles.Name.int])
self.contactChanged(contact.id)
proc setNewData*(self: ContactList, contactList: seq[Profile]) = proc setNewData*(self: ContactList, contactList: seq[Profile]) =
self.beginResetModel() self.beginResetModel()

View File

@ -1,12 +1,15 @@
import NimQml import NimQml
import chronicles import chronicles
import ../../../status/profile/profile import ../../../status/profile/profile
import ../../../status/libstatus/types
import std/wrapnils
QtObject: QtObject:
type ProfileInfoView* = ref object of QObject type ProfileInfoView* = ref object of QObject
username*: string username*: string
identicon*: string identicon*: string
address*: string address*: string
identityImage*: IdentityImage
pubKey*: string pubKey*: string
appearance*: int appearance*: int
ensVerified*: bool ensVerified*: bool
@ -24,11 +27,14 @@ QtObject:
result.username = "" result.username = ""
result.identicon = "" result.identicon = ""
result.appearance = 0 result.appearance = 0
result.identityImage = IdentityImage()
result.ensVerified = false result.ensVerified = false
result.setup result.setup
proc profileChanged*(self: ProfileInfoView) {.signal.} proc profileChanged*(self: ProfileInfoView) {.signal.}
proc identityImageChanged*(self: ProfileInfoView) {.signal.}
proc setProfile*(self: ProfileInfoView, profile: Profile) = proc setProfile*(self: ProfileInfoView, profile: Profile) =
self.username = profile.username self.username = profile.username
self.identicon = profile.identicon self.identicon = profile.identicon
@ -36,8 +42,17 @@ QtObject:
self.pubKey = profile.id self.pubKey = profile.id
self.address = profile.address self.address = profile.address
self.ensVerified = profile.ensVerified self.ensVerified = profile.ensVerified
self.identityImage = profile.identityImage
self.profileChanged() self.profileChanged()
proc setIdentityImage*(self: ProfileInfoView, identityImage: IdentityImage) =
self.identityImage = identityImage
self.identityImageChanged()
proc removeIdentityImage*(self: ProfileInfoView) =
self.identityImage = IdentityImage()
self.identityImageChanged()
proc username*(self: ProfileInfoView): string {.slot.} = result = self.username proc username*(self: ProfileInfoView): string {.slot.} = result = self.username
QtProperty[string] username: QtProperty[string] username:
read = username read = username
@ -59,6 +74,30 @@ QtObject:
read = identicon read = identicon
notify = profileChanged notify = profileChanged
proc thumbnailImage*(self: ProfileInfoView): string {.slot.} =
if (?.self.identityImage.thumbnail != ""):
result = self.identityImage.thumbnail
else:
result = self.identicon
QtProperty[string] thumbnailImage:
read = thumbnailImage
notify = identityImageChanged
proc largeImage*(self: ProfileInfoView): string {.slot.} =
if (?.self.identityImage.large != ""):
result = self.identityImage.large
else:
result = self.identicon
QtProperty[string] largeImage:
read = largeImage
notify = identityImageChanged
proc hasIdentityImage*(self: ProfileInfoView): bool {.slot.} =
result = (?.self.identityImage.thumbnail != "")
QtProperty[bool] hasIdentityImage:
read = hasIdentityImage
notify = identityImageChanged
proc pubKey*(self: ProfileInfoView): string {.slot.} = self.pubKey proc pubKey*(self: ProfileInfoView): string {.slot.} = self.pubKey
QtProperty[string] pubKey: QtProperty[string] pubKey:

View File

@ -0,0 +1,8 @@
import strutils, os
proc formatImagePath*(imagePath: string): string =
var image: string = replace(imagePath, "file://", "")
if defined(windows):
# Windows doesn't work with paths starting with a slash
image.removePrefix('/')
return image

View File

@ -19,7 +19,7 @@ proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] =
var accounts = status_accounts.generateAddresses() var accounts = status_accounts.generateAddresses()
for account in accounts.mitems: for account in accounts.mitems:
account.name = status_accounts.generateAlias(account.derived.whisper.publicKey) account.name = status_accounts.generateAlias(account.derived.whisper.publicKey)
account.photoPath = status_accounts.generateIdenticon(account.derived.whisper.publicKey) account.identicon = status_accounts.generateIdenticon(account.derived.whisper.publicKey)
self.generatedAddresses.add(account) self.generatedAddresses.add(account)
result = self.generatedAddresses result = self.generatedAddresses
@ -41,7 +41,7 @@ proc importMnemonic*(self: AccountModel, mnemonic: string): GeneratedAccount =
let importedAccount = status_accounts.multiAccountImportMnemonic(mnemonic) let importedAccount = status_accounts.multiAccountImportMnemonic(mnemonic)
importedAccount.derived = status_accounts.deriveAccounts(importedAccount.id) importedAccount.derived = status_accounts.deriveAccounts(importedAccount.id)
importedAccount.name = status_accounts.generateAlias(importedAccount.derived.whisper.publicKey) importedAccount.name = status_accounts.generateAlias(importedAccount.derived.whisper.publicKey)
importedAccount.photoPath = status_accounts.generateIdenticon(importedAccount.derived.whisper.publicKey) importedAccount.identicon = status_accounts.generateIdenticon(importedAccount.derived.whisper.publicKey)
result = importedAccount result = importedAccount
proc reset*(self: AccountModel) = proc reset*(self: AccountModel) =

View File

@ -67,9 +67,34 @@ proc initNode*() =
discard $nim_status.initKeystore(KEYSTOREDIR) discard $nim_status.initKeystore(KEYSTOREDIR)
proc parseIdentityImage*(images: JsonNode): IdentityImage =
result = IdentityImage()
if (images.kind != JNull):
for image in images:
if (image["type"].getStr == "thumbnail"):
# TODO check if this can be url or if it's always uri
result.thumbnail = image["uri"].getStr
elif (image["type"].getStr == "large"):
result.large = image["uri"].getStr
proc openAccounts*(): seq[NodeAccount] = proc openAccounts*(): seq[NodeAccount] =
let strNodeAccounts = $nim_status.openAccounts(DATADIR) let strNodeAccounts = nim_status.openAccounts(DATADIR).parseJson
result = Json.decode(strNodeAccounts, seq[NodeAccount]) # FIXME fix serialization
result = @[]
if (strNodeAccounts.kind != JNull):
for account in strNodeAccounts:
let nodeAccount = NodeAccount(
name: account["name"].getStr,
timestamp: account["timestamp"].getInt,
keyUid: account["key-uid"].getStr,
identicon: account["identicon"].getStr,
keycardPairing: account["keycard-pairing"].getStr
)
if (account{"images"}.kind != JNull):
nodeAccount.identityImage = parseIdentityImage(account["images"])
result.add(nodeAccount)
proc saveAccountAndLogin*( proc saveAccountAndLogin*(
account: GeneratedAccount, account: GeneratedAccount,
@ -91,7 +116,7 @@ proc saveAccountAndLogin*(
"public-key": account.derived.whisper.publicKey, "public-key": account.derived.whisper.publicKey,
"address": account.derived.whisper.address, "address": account.derived.whisper.address,
"name": account.name, "name": account.name,
"photo-path": account.photoPath, "identicon": account.identicon,
"path": constants.PATH_WHISPER, "path": constants.PATH_WHISPER,
"chat": true "chat": true
} }
@ -127,7 +152,7 @@ proc getAccountData*(account: GeneratedAccount): JsonNode =
result = %* { result = %* {
"name": account.name, "name": account.name,
"address": account.address, "address": account.address,
"photo-path": account.photoPath, "identicon": account.identicon,
"key-uid": account.keyUid, "key-uid": account.keyUid,
"keycard-pairing": nil "keycard-pairing": nil
} }
@ -148,7 +173,7 @@ proc getAccountSettings*(account: GeneratedAccount, defaultNetworks: JsonNode, i
"latest-derived-path": 0, "latest-derived-path": 0,
"networks/networks": defaultNetworks, "networks/networks": defaultNetworks,
"currency": "usd", "currency": "usd",
"photo-path": account.photoPath, "identicon": account.identicon,
"waku-enabled": true, "waku-enabled": true,
"wallet/visible-tokens": { "wallet/visible-tokens": {
"mainnet": ["SNT"] "mainnet": ["SNT"]
@ -336,3 +361,22 @@ proc deriveAccounts*(accountId: string): MultiAccounts =
proc logout*(): StatusGoError = proc logout*(): StatusGoError =
result = Json.decode($nim_status.logout(), StatusGoError) result = Json.decode($nim_status.logout(), StatusGoError)
proc storeIdentityImage*(keyUID: string, imagePath: string, aX, aY, bX, bY: int): IdentityImage =
let response = callPrivateRPC("multiaccounts_storeIdentityImage", %* [keyUID, imagePath, aX, aY, bX, bY]).parseJson
result = parseIdentityImage(response{"result"})
proc getIdentityImage*(keyUID: string): IdentityImage =
try:
let response = callPrivateRPC("multiaccounts_getIdentityImages", %* [keyUID]).parseJson
result = parseIdentityImage(response{"result"})
except Exception as e:
error "Error getting identity image", msg=e.msg
proc deleteIdentityImage*(keyUID: string): string =
try:
let response = callPrivateRPC("multiaccounts_deleteIdentityImage", %* [keyUID]).parseJson
result = ""
except Exception as e:
error "Error getting identity image", msg=e.msg
result = e.msg

View File

@ -15,6 +15,7 @@ proc getOurInstallations*(useCached: bool = true): JsonNode =
result = installations result = installations
proc syncDevices*(preferredName: string): string = proc syncDevices*(preferredName: string): string =
# TODO change this to identicon when status-go is updated
let photoPath = "" let photoPath = ""
result = callPrivateRPC("syncDevices".prefix, %* [preferredName, photoPath]) result = callPrivateRPC("syncDevices".prefix, %* [preferredName, photoPath])

View File

@ -42,12 +42,17 @@ type MultiAccounts* = object
defaultWallet* {.serializedFieldName(PATH_DEFAULT_WALLET).}: DerivedAccount defaultWallet* {.serializedFieldName(PATH_DEFAULT_WALLET).}: DerivedAccount
eip1581* {.serializedFieldName(PATH_EIP_1581).}: DerivedAccount eip1581* {.serializedFieldName(PATH_EIP_1581).}: DerivedAccount
type
IdentityImage* = ref object
thumbnail*: string
large*: string
type type
Account* = ref object of RootObj Account* = ref object of RootObj
name*: string name*: string
keyUid* {.serializedFieldName("key-uid").}: string keyUid* {.serializedFieldName("key-uid").}: string
photoPath* {.serializedFieldName("photo-path").}: string identityImage*: IdentityImage
identicon*: string
type type
NodeAccount* = ref object of Account NodeAccount* = ref object of Account
@ -66,7 +71,8 @@ type
# serializedFieldName pragma would need to be different # serializedFieldName pragma would need to be different
name*: string name*: string
keyUid*: string keyUid*: string
photoPath*: string identicon*: string
identityImage*: IdentityImage
type RpcError* = ref object type RpcError* = ref object
code*: int code*: int
@ -88,10 +94,10 @@ type
error*: RpcError error*: RpcError
proc toAccount*(account: GeneratedAccount): Account = proc toAccount*(account: GeneratedAccount): Account =
result = Account(name: account.name, photoPath: account.photoPath, keyUid: account.address) result = Account(name: account.name, identityImage: account.identityImage, identicon: account.identicon, keyUid: account.address)
proc toAccount*(account: NodeAccount): Account = proc toAccount*(account: NodeAccount): Account =
result = Account(name: account.name, photoPath: account.photoPath, keyUid: account.keyUid) result = Account(name: account.name, identityImage: account.identityImage, identicon: account.identicon, keyUid: account.keyUid)
type AccountArgs* = ref object of Args type AccountArgs* = ref object of Args
account*: Account account*: Account
@ -247,4 +253,4 @@ proc getNodes*(self: FleetConfig, fleet: Fleet, nodeType: FleetNodes = FleetNode
result = toSeq(self.fleet[$fleet][$nodeType].values) result = toSeq(self.fleet[$fleet][$nodeType].values)
proc getMailservers*(self: FleetConfig, fleet: Fleet): Table[string, string] = proc getMailservers*(self: FleetConfig, fleet: Fleet): Table[string, string] =
result = self.fleet[$fleet][$FleetNodes.Mailservers] result = self.fleet[$fleet][$FleetNodes.Mailservers]

View File

@ -17,3 +17,12 @@ proc logout*(self: ProfileModel) =
proc getLinkPreviewWhitelist*(self: ProfileModel): JsonNode = proc getLinkPreviewWhitelist*(self: ProfileModel): JsonNode =
result = status_settings.getLinkPreviewWhitelist() result = status_settings.getLinkPreviewWhitelist()
proc storeIdentityImage*(self: ProfileModel, keyUID: string, imagePath: string, aX, aY, bX, bY: int): IdentityImage =
result = status_accounts.storeIdentityImage(keyUID, imagePath, aX, aY, bX, bY)
proc getIdentityImage*(self: ProfileModel, keyUID: string): IdentityImage =
result = status_accounts.getIdentityImage(keyUID)
proc deleteIdentityImage*(self: ProfileModel, keyUID: string): string =
result = status_accounts.deleteIdentityImage(keyUID)

View File

@ -4,6 +4,7 @@ import ../libstatus/types
type Profile* = ref object type Profile* = ref object
id*, alias*, username*, identicon*, address*, ensName*, localNickname*: string id*, alias*, username*, identicon*, address*, ensName*, localNickname*: string
ensVerified*: bool ensVerified*: bool
identityImage*: IdentityImage
ensVerifiedAt*, ensVerificationRetries*, appearance*: int ensVerifiedAt*, ensVerificationRetries*, appearance*: int
systemTags*: seq[string] systemTags*: seq[string]
@ -17,7 +18,7 @@ proc toProfileModel*(account: Account): Profile =
result = Profile( result = Profile(
id: "", id: "",
username: account.name, username: account.name,
identicon: account.photoPath, identicon: account.identicon,
alias: account.name, alias: account.name,
ensName: "", ensName: "",
ensVerified: false, ensVerified: false,
@ -36,6 +37,7 @@ proc toProfileModel*(profile: JsonNode): Profile =
id: profile["id"].str, id: profile["id"].str,
username: profile["alias"].str, username: profile["alias"].str,
identicon: profile["identicon"].str, identicon: profile["identicon"].str,
identityImage: IdentityImage(),
address: profile["id"].str, address: profile["id"].str,
alias: profile["alias"].str, alias: profile["alias"].str,
ensName: "", ensName: "",
@ -51,3 +53,8 @@ proc toProfileModel*(profile: JsonNode): Profile =
if profile.hasKey("localNickname"): if profile.hasKey("localNickname"):
result.localNickname = profile["localNickname"].str result.localNickname = profile["localNickname"].str
if profile.hasKey("images"):
if profile["images"].hasKey("thumbnail"):
result.identityImage.thumbnail = profile["images"]["thumbnail"]["uri"].str
if profile["images"].hasKey("large"):
result.identityImage.large = profile["images"]["large"]["uri"].str

View File

@ -41,7 +41,7 @@ Item {
onLinkActivated: function (linkClicked) { onLinkActivated: function (linkClicked) {
switch (linkClicked) { switch (linkClicked) {
case "shareKey": case "shareKey":
openProfilePopup(profileModel.profile.username, profileModel.profile.pubKey, profileModel.profile.identicon); openProfilePopup(profileModel.profile.username, profileModel.profile.pubKey, profileModel.profile.thumbnailImage);
break; break;
case "invite": inviteFriendsPopup.open(); break; case "invite": inviteFriendsPopup.open(); break;
default: //no idea what was clicked default: //no idea what was clicked

View File

@ -1,4 +1,4 @@
import QtQuick 2.3 import QtQuick 2.13
import "../../../../shared" import "../../../../shared"
import "../../../../imports" import "../../../../imports"
import "./MessageComponents" import "./MessageComponents"
@ -25,6 +25,7 @@ Item {
property bool timeout: false property bool timeout: false
property string linkUrls: "" property string linkUrls: ""
property string imageUrls: "" property string imageUrls: ""
property bool placeholderMessage: false
property string authorCurrentMsg: "authorCurrentMsg" property string authorCurrentMsg: "authorCurrentMsg"
property string authorPrevMsg: "authorPrevMsg" property string authorPrevMsg: "authorPrevMsg"
@ -48,6 +49,25 @@ Item {
property var imageClick: function () {} property var imageClick: function () {}
property var scrollToBottom: function () {} property var scrollToBottom: function () {}
property string userPubKey: {
if (contentType === Constants.chatIdentifier) {
return chatId
}
return fromAuthor
}
property bool useLargeImage: contentType === Constants.chatIdentifier
property string profileImageSource: !placeholderMessage && chatView.getProfileImage(userPubKey, isCurrentUser, useLargeImage) || ""
Connections {
enabled: !placeholderMessage
target: profileModel.contacts.list
onContactChanged: {
if (pubkey === fromAuthor) {
profileImageSource = chatView.getProfileImage(userPubKey, isCurrentUser, useLargeImage)
}
}
}
id: root id: root
width: parent.width width: parent.width
@ -82,13 +102,13 @@ Item {
messageContextMenu.isProfile = !!isProfileClick messageContextMenu.isProfile = !!isProfileClick
messageContextMenu.isSticker = isSticker messageContextMenu.isSticker = isSticker
messageContextMenu.emojiOnly = emojiOnly messageContextMenu.emojiOnly = emojiOnly
messageContextMenu.show(userName, fromAuthor, identicon, "", nickname) messageContextMenu.show(userName, fromAuthor, root.profileImageSource || identicon, "", nickname)
// Position the center of the menu where the mouse is // Position the center of the menu where the mouse is
messageContextMenu.x = messageContextMenu.x - messageContextMenu.width / 2 messageContextMenu.x = messageContextMenu.x - messageContextMenu.width / 2
} }
Loader { Loader {
active :true active: true
width: parent.width width: parent.width
sourceComponent: { sourceComponent: {
switch(contentType) { switch(contentType) {
@ -165,6 +185,7 @@ Item {
id: channelIdentifierComponent id: channelIdentifierComponent
ChannelIdentifier { ChannelIdentifier {
authorCurrentMsg: root.authorCurrentMsg authorCurrentMsg: root.authorCurrentMsg
profileImage: profileImageSource
} }
} }
@ -173,7 +194,7 @@ Item {
id: privateGroupHeaderComponent id: privateGroupHeaderComponent
StyledText { StyledText {
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: { text: {
return `<html>`+ return `<html>`+
`<head>`+ `<head>`+
`<style type="text/css">`+ `<style type="text/css">`+

View File

@ -6,8 +6,10 @@ Item {
property string authorCurrentMsg: "authorCurrentMsg" property string authorCurrentMsg: "authorCurrentMsg"
property int verticalMargin: 50 property int verticalMargin: 50
property string profileImage
id: channelIdentifier id: channelIdentifier
visible: authorCurrentMsg == "" visible: authorCurrentMsg === ""
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: this.visible ? verticalMargin : 0 anchors.topMargin: this.visible ? verticalMargin : 0
@ -28,15 +30,13 @@ Item {
return chatsModel.activeChannel.color return chatsModel.activeChannel.color
} }
Image { RoundedImage {
visible: chatsModel.activeChannel.chatType === Constants.chatTypeOneToOne visible: chatsModel.activeChannel.chatType === Constants.chatTypeOneToOne
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 120 width: 120
height: 120 height: 120
fillMode: Image.PreserveAspectFit source: channelIdentifier.profileImage || chatsModel.activeChannel.identicon
source: chatsModel.activeChannel.identicon
mipmap: true
smooth: false smooth: false
antialiasing: true antialiasing: true
} }
@ -71,7 +71,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
Rectangle { Item {
id: channelDescription id: channelDescription
visible: descText.visible visible: descText.visible
width: visible ? 330 : 0 width: visible ? 330 : 0
@ -79,7 +79,6 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: channelName.bottom anchors.top: channelName.bottom
anchors.topMargin: 16 anchors.topMargin: 16
color: Style.current.transparent
StyledText { StyledText {
id: descText id: descText

View File

@ -50,7 +50,8 @@ Item {
if (link.startsWith('//')) { if (link.startsWith('//')) {
let pk = link.replace("//", ""); let pk = link.replace("//", "");
openProfilePopup(chatsModel.userNameOrAlias(pk), pk, utilsModel.generateIdenticon(pk)) const userProfileImage = chatView.getProfileImage(pk)
openProfilePopup(chatsModel.userNameOrAlias(pk), pk, userProfileImage || utilsModel.generateIdenticon(pk))
return; return;
} }

View File

@ -7,21 +7,24 @@ Loader {
height: active ? item.height : 0 height: active ? item.height : 0
sourceComponent: Component { sourceComponent: Component {
Rectangle { Item {
id: chatImage id: chatImage
width: identiconImage.width width: identiconImage.width
height: identiconImage.height height: identiconImage.height
border.width: 1
border.color: Style.current.border
radius: 50
Image { RoundedImage {
id: identiconImage id: identiconImage
width: 36 width: 36
height: 36 height: 36
fillMode: Image.PreserveAspectFit border.width: 1
source: !isCurrentUser ? identicon : profileModel.profile.identicon border.color: Style.current.border
mipmap: true source: {
if (profileImageSource) {
return profileImageSource
}
return !isCurrentUser ? identicon : profileModel.profile.identicon
}
smooth: false smooth: false
antialiasing: true antialiasing: true

View File

@ -40,7 +40,8 @@ Rectangle {
groupInfoPopup.open() groupInfoPopup.open()
break; break;
case Constants.chatTypeOneToOne: case Constants.chatTypeOneToOne:
openProfilePopup(chatsModel.activeChannel.name, chatsModel.activeChannel.id, chatsModel.activeChannel.identicon) const profileImage = chatView.getProfileImage(chatsModel.activeChannel.id)
openProfilePopup(chatsModel.activeChannel.name, chatsModel.activeChannel.id, profileImage || chatsModel.activeChannel.identicon)
break; break;
} }
} }

View File

@ -37,6 +37,19 @@ SplitView {
chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount
} }
function getProfileImage(pubkey, isCurrentUser, useLargeImage) {
if (isCurrentUser || (isCurrentUser === undefined && pubkey === profileModel.profile.pubKey)) {
return profileModel.profile.thumbnailImage
}
const index = profileModel.contacts.list.getContactIndexByPubkey(pubkey)
if (index === -1) {
return
}
return profileModel.contacts.list.rowData(index, useLargeImage ? "largeImage" : "thumbnailImage")
}
function openProfilePopup(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, parentPopup){ function openProfilePopup(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, parentPopup){
var popup = profilePopupComponent.createObject(chatView); var popup = profilePopupComponent.createObject(chatView);
if(parentPopup){ if(parentPopup){

View File

@ -20,6 +20,18 @@ Rectangle {
property bool muted: false property bool muted: false
property bool hovered: false property bool hovered: false
property string profileImage: chatType === Constants.chatTypeOneToOne ? chatView.getProfileImage(chatId) || "" : ""
Connections {
enabled: chatType === Constants.chatTypeOneToOne
target: profileModel.contacts.list
onContactChanged: {
if (pubkey === wrapper.chatId) {
wrapper.profileImage = chatView.getProfileImage(wrapper.chatId)
}
}
}
id: wrapper id: wrapper
color: { color: {
if (ListView.isCurrentItem || wrapper.hovered) { if (ListView.isCurrentItem || wrapper.hovered) {
@ -42,7 +54,7 @@ Rectangle {
width: !isCompact ? 40 : 20 width: !isCompact ? 40 : 20
chatName: wrapper.name chatName: wrapper.name
chatType: wrapper.chatType chatType: wrapper.chatType
identicon: wrapper.identicon identicon: wrapper.profileImage || wrapper.identicon
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: !isCompact ? Style.current.padding : Style.current.smallPadding anchors.leftMargin: !isCompact ? Style.current.padding : Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter

View File

@ -156,7 +156,8 @@ ScrollView {
chatsModel.setActiveChannelByIndex(channelContextMenu.channelIndex) chatsModel.setActiveChannelByIndex(channelContextMenu.channelIndex)
chatGroupsListView.currentIndex = channelContextMenu.channelIndex chatGroupsListView.currentIndex = channelContextMenu.channelIndex
if (channelContextMenu.chatType === Constants.chatTypeOneToOne) { if (channelContextMenu.chatType === Constants.chatTypeOneToOne) {
return openProfilePopup(channelContextMenu.chatName, channelContextMenu.chatId, channelContextMenu.chatIdenticon) const userProfileImage = chatView.getProfileImage(channelContextMenu.chatId)
return openProfilePopup(channelContextMenu.chatName, channelContextMenu.chatId, userProfileImage || channelContextMenu.chatIdenticon)
} }
if (channelContextMenu.chatType === Constants.chatTypePrivateGroupChat) { if (channelContextMenu.chatType === Constants.chatTypePrivateGroupChat) {
return groupInfoPopup.open() return groupInfoPopup.open()

View File

@ -37,6 +37,7 @@ ModalPopup {
pubKey: profileModel.contacts.list.rowData(i, "pubKey"), pubKey: profileModel.contacts.list.rowData(i, "pubKey"),
address: profileModel.contacts.list.rowData(i, "address"), address: profileModel.contacts.list.rowData(i, "address"),
identicon: profileModel.contacts.list.rowData(i, "identicon"), identicon: profileModel.contacts.list.rowData(i, "identicon"),
thumbnailImage: profileModel.contacts.list.rowData(i, "thumbnailImage"),
isUser: false, isUser: false,
isContact: profileModel.contacts.list.rowData(i, "isContact") !== "false" isContact: profileModel.contacts.list.rowData(i, "isContact") !== "false"
}); });
@ -47,6 +48,7 @@ ModalPopup {
pubKey: profileModel.profile.pubKey, pubKey: profileModel.profile.pubKey,
address: "", address: "",
identicon: profileModel.profile.identicon, identicon: profileModel.profile.identicon,
thumbnailImage: profileModel.profile.thumbnailImage,
isUser: true isUser: true
}); });
noContactsRect.visible = !profileModel.contacts.list.hasAddedContacts(); noContactsRect.visible = !profileModel.contacts.list.hasAddedContacts();
@ -186,7 +188,7 @@ ModalPopup {
name: !model.name.endsWith(".eth") && !!model.localNickname ? name: !model.name.endsWith(".eth") && !!model.localNickname ?
model.localNickname : Utils.removeStatusEns(model.name) model.localNickname : Utils.removeStatusEns(model.name)
address: model.address address: model.address
identicon: model.identicon identicon: model.thumbnailImage || model.identicon
onItemChecked: function(pubKey, itemChecked){ onItemChecked: function(pubKey, itemChecked){
var idx = pubKeys.indexOf(pubKey) var idx = pubKeys.indexOf(pubKey)
if(itemChecked){ if(itemChecked){

View File

@ -30,6 +30,7 @@ ModalPopup {
pubKey: profileModel.contacts.list.rowData(i, "pubKey"), pubKey: profileModel.contacts.list.rowData(i, "pubKey"),
address: profileModel.contacts.list.rowData(i, "address"), address: profileModel.contacts.list.rowData(i, "address"),
identicon: profileModel.contacts.list.rowData(i, "identicon"), identicon: profileModel.contacts.list.rowData(i, "identicon"),
thumbnailImage: profileModel.contacts.list.rowData(i, "thumbnailImage"),
isUser: false isUser: false
}); });
} }
@ -211,7 +212,7 @@ ModalPopup {
name: model.name.endsWith(".eth") || !model.localNickname ? name: model.name.endsWith(".eth") || !model.localNickname ?
Utils.removeStatusEns(model.name) : model.localNickname Utils.removeStatusEns(model.name) : model.localNickname
address: model.address address: model.address
identicon: model.identicon identicon: model.thumbnailImage || model.identicon
onItemChecked: function(pubKey, itemChecked){ onItemChecked: function(pubKey, itemChecked){
var idx = pubKeys.indexOf(pubKey) var idx = pubKeys.indexOf(pubKey)
if(itemChecked){ if(itemChecked){
@ -278,7 +279,7 @@ ModalPopup {
StatusImageIdenticon { StatusImageIdenticon {
id: identicon id: identicon
anchors.left: parent.left anchors.left: parent.left
source: model.identicon source: chatView.getProfileImage(model.pubKey)|| model.identicon
} }
StyledText { StyledText {
@ -293,7 +294,10 @@ ModalPopup {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: openProfilePopup(model.userName, model.pubKey, model.identicon, '', contactRow.nickname) onClicked: {
const userProfileImage = chatView.getProfileImage(model.pubKey)
openProfilePopup(model.userName, model.pubKey, userProfileImage || model.identicon, '', contactRow.nickname)
}
} }
} }

View File

@ -21,7 +21,7 @@ PopupMenu {
property var fromAuthor: "" property var fromAuthor: ""
property var text: "" property var text: ""
function show(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam){ function show(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam) {
userName = userNameParam || "" userName = userNameParam || ""
nickname = nicknameParam || "" nickname = nicknameParam || ""
fromAuthor = fromAuthorParam || "" fromAuthor = fromAuthorParam || ""

View File

@ -138,7 +138,7 @@ ModalPopup {
isUser: false isUser: false
name: model.name name: model.name
address: model.address address: model.address
identicon: model.identicon identicon: model.thumbnailImage || model.identicon
showListSelector: true showListSelector: true
onItemChecked: function(pubKey, itemChecked){ onItemChecked: function(pubKey, itemChecked){
chatsModel.joinChat(pubKey, Constants.chatTypeOneToOne); chatsModel.joinChat(pubKey, Constants.chatTypeOneToOne);

View File

@ -49,21 +49,14 @@ ModalPopup {
header: Item { header: Item {
height: children[0].height height: children[0].height
width: parent.width width: parent.width
Rectangle { RoundedImage {
id: profilePic id: profilePic
width: 40 width: 40
height: 40 height: 40
radius: 30 border.color: Style.current.border
border.color: "#10000000"
border.width: 1 border.width: 1
color: Style.current.transparent
anchors.top: parent.top anchors.top: parent.top
SVGImage { source: identicon
width: parent.width
height: parent.height
fillMode: Image.PreserveAspectFit
source: identicon
}
} }
StyledTextEdit { StyledTextEdit {

View File

@ -45,12 +45,7 @@ SplitView {
// This list needs to match LeftTab/constants.js // This list needs to match LeftTab/constants.js
// Would be better if we could make them match automatically // Would be better if we could make them match automatically
MyProfileContainer { MyProfileContainer {}
username: profileModel.profile.username
ensName: profileModel.ens.preferredUsername
identicon: profileModel.profile.identicon
pubkey: profileModel.profile.pubKey
}
onCurrentIndexChanged: { onCurrentIndexChanged: {
if(visibleChildren[0] === ensContainer){ if(visibleChildren[0] === ensContainer){

View File

@ -81,7 +81,7 @@ ScrollView {
identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAb0lEQVR4Ae3UQQqAIBRF0Wj9ba9Bq6l5JBQqfn/ngDMH3YS3AAB/tO3H+XRG3b9bR/+gVoREI2RapVXpfd5+X5oXERKNkHS+rk3tOpWkeREh0QiZVu91ql2zNC8iJBoh0yqtSqt1slpCghICANDPBc0ESPh0bHkHAAAAAElFTkSuQmCC" identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAb0lEQVR4Ae3UQQqAIBRF0Wj9ba9Bq6l5JBQqfn/ngDMH3YS3AAB/tO3H+XRG3b9bR/+gVoREI2RapVXpfd5+X5oXERKNkHS+rk3tOpWkeREh0QiZVu91ql2zNC8iJBoh0yqtSqt1slpCghICANDPBc0ESPh0bHkHAAAAAElFTkSuQmCC"
message: qsTr("Blockchains will drop search costs, causing a kind of decomposition that allows you to have markets of entities that are horizontally segregated and vertically segregated.") message: qsTr("Blockchains will drop search costs, causing a kind of decomposition that allows you to have markets of entities that are horizontally segregated and vertically segregated.")
contentType: Constants.messageType contentType: Constants.messageType
placeholderMessage: true
} }
} }

View File

@ -0,0 +1,150 @@
import QtQuick 2.13
import QtQuick.Dialogs 1.3
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
ModalPopup {
property string selectedImage // selectedImage is for us to be able to analyze it before setting it as current
property string uploadError
id: popup
title: qsTr("Profile picture")
onClosed: {
destroy()
}
onSelectedImageChanged: {
if (!selectedImage) {
return
}
cropImageModal.open()
}
Item {
anchors.fill: parent
RoundedImage {
id: profilePic
source: profileModel.profile.largeImage
width: 160
height: 160
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
border.width: 1
border.color: Style.current.border
onClicked: imageDialog.open()
}
StyledText {
visible: !!uploadError
text: uploadError
anchors.left: parent.left
anchors.right: parent.right
anchors.top: profilePic.bottom
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 13
wrapMode: Text.WordWrap
anchors.topMargin: 13
font.weight: Font.Thin
color: Style.current.danger
}
ModalPopup {
id: cropImageModal
width: image.width + 50
height: image.height + 170
title: qsTr("Crop your image (optionnal)")
Image {
id: image
width: 400
source: popup.selectedImage
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
fillMode: Image.PreserveAspectFit
}
ImageCropper {
id: imageCropper
x: image.x
y: image.y
image: image
}
footer: StatusButton {
id: doUploadBtn
text: qsTr("Finish")
anchors.right: parent.right
anchors.bottom: parent.bottom
onClicked: {
const aXPercent = imageCropper.selectorRectangle.x / image.width
const aYPercent = imageCropper.selectorRectangle.y / image.height
const bXPercent = (imageCropper.selectorRectangle.x + imageCropper.selectorRectangle.width) / image.width
const bYPercent = (imageCropper.selectorRectangle.y + imageCropper.selectorRectangle.height) / image.height
const aX = Math.round(aXPercent * image.sourceSize.width)
const aY = Math.round(aYPercent * image.sourceSize.height)
const bX = Math.round(bXPercent * image.sourceSize.width)
const bY = Math.round(bYPercent * image.sourceSize.height)
uploadError = profileModel.uploadNewProfilePic(selectedImage, aX, aY, bX, bY)
cropImageModal.close()
}
}
}
}
footer: Item {
anchors.fill:parent
StatusButton {
visible: profileModel.profile.hasIdentityImage
type: "secondary"
flat: true
color: Style.current.danger
text: qsTr("Remove")
anchors.right: uploadBtn.left
anchors.rightMargin: Style.current.padding
anchors.bottom: parent.bottom
onClicked: {
uploadError = profileModel.deleteProfilePic()
}
}
StatusButton {
id: uploadBtn
text: qsTr("Upload")
anchors.right: parent.right
anchors.bottom: parent.bottom
onClicked: {
imageDialog.open()
}
FileDialog {
id: imageDialog
//% "Please choose an image"
title: qsTrId("please-choose-an-image")
folder: shortcuts.pictures
nameFilters: [
//% "Image files (*.jpg *.jpeg *.png)"
qsTrId("image-files----jpg---jpeg---png-")
]
onAccepted: {
selectedImage = imageDialog.fileUrls[0]
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -25,7 +25,7 @@ ListView {
name: Utils.removeStatusEns(model.name) name: Utils.removeStatusEns(model.name)
address: model.address address: model.address
localNickname: model.localNickname localNickname: model.localNickname
identicon: model.identicon identicon: model.thumbnailImage || model.identicon
isContact: model.isContact isContact: model.isContact
isBlocked: model.isBlocked isBlocked: model.isBlocked
selectable: contactList.selectable selectable: contactList.selectable

View File

@ -6,15 +6,19 @@ import "../../../../shared"
import "../../../../shared/status" import "../../../../shared/status"
Item { Item {
property string username: "Jotaro Kujo" property string ensName: profileModel.profile.preferredUsername || ""
property string identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAhklEQVR4nOzWwQ1AQBgFYUQvelKHMtShJ9VwFyvrsExe5jvKXiYv+WPoQhhCYwiNITSG0MSEjLUPt3097r7P09L/8f4qZhFDaAyhqboIT76+TiUxixhCYwhN9b/WW6Xr1ErMIobQGEJjCI0hNIbQGEJjCI0haiRmEUNoDKExhMYQmjMAAP//B2kXcP2uDV8AAAAASUVORK5CYII=" property string username: profileModel.profile.username
property string pubkey: "0x04d8c07dd137bd1b73a6f51df148b4f77ddaa11209d36e43d8344c0a7d6db1cad6085f27cfb75dd3ae21d86ceffebe4cf8a35b9ce8d26baa19dc264efe6d8f221b" property string pubkey: profileModel.profile.pubKey
property string ensName: "joestar.eth"
id: profileHeaderContent id: profileHeaderContent
height: parent.height height: parent.height
Layout.fillWidth: true Layout.fillWidth: true
Component {
id: changeProfileModalComponent
ChangeProfilePicModal {}
}
Item { Item {
id: profileImgNameContainer id: profileImgNameContainer
anchors.top: parent.top anchors.top: parent.top
@ -26,43 +30,58 @@ Item {
height: this.childrenRect.height height: this.childrenRect.height
Rectangle { Item {
id: profileImg id: profileImgContainer
width: identiconImage.width width: profileImg.width
height: identiconImage.height height: profileImg.height
border.width: 1
border.color: Style.current.border
radius: 50
color: Style.current.background
Image { RoundedImage {
id: identiconImage id: profileImg
width: 60 width: 64
height: 60 height: 64
fillMode: Image.PreserveAspectFit border.width: 1
source: identicon border.color: Style.current.border
mipmap: true source: profileModel.profile.thumbnailImage || ""
smooth: false smooth: false
antialiasing: true antialiasing: true
} }
RoundedIcon {
source: "../../../img/pencil.svg"
anchors.bottom: parent.bottom
anchors.bottomMargin: -3
anchors.right: parent.right
anchors.rightMargin: -3
width: 24
height: 24
border.width: 1
border.color: Style.current.background
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const popup = changeProfileModalComponent.createObject(profileHeaderContent);
popup.open()
}
}
} }
StyledText { StyledText {
id: profileName id: profileName
text: ensName !== "" ? ensName : username text: ensName !== "" ? ensName : username
anchors.left: profileImg.right anchors.left: profileImgContainer.right
anchors.leftMargin: 8 anchors.leftMargin: 8
anchors.top: profileImg.top anchors.top: profileImgContainer.top
anchors.topMargin: 4 font.weight: Font.Medium
font.family: "Inter" font.pixelSize: 15
font.weight: Font.Bold
font.pixelSize: 20
} }
Address { Address {
id: pubkeyText id: pubkeyText
text: ensName !== "" ? username : pubkey text: ensName !== "" ? username : pubkey
anchors.bottom: profileImg.bottom anchors.bottom: profileImgContainer.bottom
anchors.left: profileName.left anchors.left: profileName.left
anchors.bottomMargin: 4 anchors.bottomMargin: 4
width: 200 width: 200
@ -85,7 +104,9 @@ Item {
Separator { Separator {
id: lineSeparator id: lineSeparator
anchors.top: profileImg.bottom anchors.top: profileImgContainer.bottom
anchors.topMargin: 36
} }
} }

4
ui/app/img/pencil.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.10144 13.5959L2.79885 10.8062C2.83108 10.6773 2.87171 10.5512 2.92034 10.4285C2.9822 10.2724 3.18194 10.2423 3.30066 10.361L5.63945 12.6998C5.75817 12.8185 5.72809 13.0183 5.572 13.0801C5.44932 13.1288 5.32315 13.1694 5.19424 13.2016L2.40461 13.899C2.22152 13.9448 2.05567 13.779 2.10144 13.5959Z" fill="white"/>
<path d="M6.32361 11.2627C6.71413 11.6532 7.3473 11.6532 7.73782 11.2627L14.2933 4.7072C14.6838 4.31668 14.6838 3.68351 14.2933 3.29299L12.7075 1.7072C12.317 1.31668 11.6838 1.31668 11.2933 1.7072L4.73782 8.26266C4.3473 8.65318 4.3473 9.28635 4.73782 9.67687L6.32361 11.2627Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 720 B

View File

@ -182,6 +182,7 @@ DISTFILES += \
app/AppLayouts/Profile/Sections/AppearanceContainer.qml \ app/AppLayouts/Profile/Sections/AppearanceContainer.qml \
app/AppLayouts/Profile/Sections/BackupSeedModal.qml \ app/AppLayouts/Profile/Sections/BackupSeedModal.qml \
app/AppLayouts/Profile/Sections/BrowserContainer.qml \ app/AppLayouts/Profile/Sections/BrowserContainer.qml \
app/AppLayouts/Profile/Sections/ChangeProfilePicModal.qml \
app/AppLayouts/Profile/Sections/MyProfileContainer.qml \ app/AppLayouts/Profile/Sections/MyProfileContainer.qml \
app/AppLayouts/Profile/Sections/SoundsContainer.qml \ app/AppLayouts/Profile/Sections/SoundsContainer.qml \
app/AppLayouts/UIComponents/UIComponents.qml \ app/AppLayouts/UIComponents/UIComponents.qml \
@ -364,8 +365,10 @@ DISTFILES += \
shared/AccountSelector.qml \ shared/AccountSelector.qml \
shared/AddButton.qml \ shared/AddButton.qml \
shared/Address.qml \ shared/Address.qml \
shared/CropCornerRectangle.qml \
shared/FormGroup.qml \ shared/FormGroup.qml \
shared/IconButton.qml \ shared/IconButton.qml \
shared/ImageCropper.qml \
shared/Input.qml \ shared/Input.qml \
shared/LabelValueRow.qml \ shared/LabelValueRow.qml \
shared/ModalPopup.qml \ shared/ModalPopup.qml \

View File

@ -35,7 +35,7 @@ Item {
StatusImageIdenticon { StatusImageIdenticon {
id: userImage id: userImage
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
source: loginModel.currentAccount.identicon source: loginModel.currentAccount.thumbnailImage
} }
StyledText { StyledText {

View File

@ -17,7 +17,7 @@ ListView {
delegate: AddressView { delegate: AddressView {
username: model.username username: model.username
address: model.address address: model.address
identicon: model.identicon identicon: model.thumbnailImage
isSelected: function (index, address) { isSelected: function (index, address) {
return addressesView.isSelected(index, address) return addressesView.isSelected(index, address)
} }

View File

@ -0,0 +1,23 @@
import QtQuick 2.13
import "../imports"
Rectangle {
id: root
width: 25
height: 25
border.width: 2
border.color: Style.current.orange
color: Utils.setColorAlpha(Style.current.orange, 0.5)
Drag.active: dragArea.drag.active
MouseArea {
id: dragArea
property int oldX
property int oldY
anchors.fill: parent
drag.target: parent
cursorShape: Qt.PointingHandCursor
}
}

107
ui/shared/ImageCropper.qml Normal file
View File

@ -0,0 +1,107 @@
import QtQuick 2.13
import "."
import "../imports"
Item {
property var image
property alias selectorRectangle: selectorRectangle
width: image.width
height: image.height
Rectangle {
id: selectorRectangle
visible: false
x: 0
y: 0
border.width: 2
border.color: Style.current.orange
color: Style.current.transparent
function initialSetup() {
topLeftCorner.x = 0
topLeftCorner.y = 0
topRightCorner.x = image.width - topRightCorner.width
topRightCorner.y = 0
bottomLeftCorner.x = 0
bottomLeftCorner.y = image.height - topRightCorner.height
bottomRightCorner.x = image.width - topRightCorner.width
bottomRightCorner.y = image.height - topRightCorner.height
selectorRectangle.width = image.width
selectorRectangle.height = image.height
}
function adjustRectangleSize() {
selectorRectangle.width = bottomRightCorner.x + bottomRightCorner.width - topLeftCorner.x
selectorRectangle.height = bottomRightCorner.y + bottomRightCorner.height - topLeftCorner.y
selectorRectangle.x = topLeftCorner.x
selectorRectangle.y = topLeftCorner.y
}
Connections {
target: image
onStatusChanged: {
if (image.status === Image.Ready) {
selectorRectangle.initialSetup()
selectorRectangle.visible = true
}
}
}
}
// Size calculations are only done on top-left and bottom-right, because the other two corners follow them
CropCornerRectangle {
id: topLeftCorner
onXChanged: {
if (x < 0) x = 0
if (x > topRightCorner.x - width) x = topRightCorner.x - width
bottomLeftCorner.x = x
selectorRectangle.adjustRectangleSize()
}
onYChanged: {
if (y < 0) y = 0
if (y > bottomRightCorner.y - height) y = bottomRightCorner.y - height
topRightCorner.y = y
selectorRectangle.adjustRectangleSize()
}
}
CropCornerRectangle {
id: topRightCorner
onXChanged: {
bottomRightCorner.x = x
}
onYChanged: {
topLeftCorner.y = y
}
}
CropCornerRectangle {
id: bottomLeftCorner
onXChanged: {
topLeftCorner.x = x
}
onYChanged: {
bottomRightCorner.y = y
}
}
CropCornerRectangle {
id: bottomRightCorner
onXChanged: {
if (x < topLeftCorner.x + topLeftCorner.width) x = topLeftCorner.x + topLeftCorner.width
if (x > image.width - width) x = image.width - width
topRightCorner.x = x
selectorRectangle.adjustRectangleSize()
}
onYChanged: {
if (y < topRightCorner.y + topRightCorner.height) y = topRightCorner.y + topRightCorner.height
if (y > image.height - height) y = image.height - height
bottomLeftCorner.y = y
selectorRectangle.adjustRectangleSize()
}
}
}

View File

@ -4,6 +4,7 @@ import "../imports"
Rectangle { Rectangle {
id: root id: root
property bool noHover: false
property alias source: image.source property alias source: image.source
property alias fillMode: image.fillMode property alias fillMode: image.fillMode
signal loaded signal loaded
@ -138,7 +139,7 @@ Rectangle {
} }
} }
MouseArea { MouseArea {
cursorShape: Qt.PointingHandCursor cursorShape: noHover ? Qt.ArrowCursor : Qt.PointingHandCursor
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
root.clicked() root.clicked()

View File

@ -4,6 +4,7 @@ import QtGraphicalEffects 1.0
Rectangle { Rectangle {
id: root id: root
signal clicked signal clicked
property bool noHover: false
property alias source: imgStickerPackThumb.source property alias source: imgStickerPackThumb.source
property alias fillMode: imgStickerPackThumb.fillMode property alias fillMode: imgStickerPackThumb.fillMode
@ -25,6 +26,7 @@ Rectangle {
ImageLoader { ImageLoader {
id: imgStickerPackThumb id: imgStickerPackThumb
noHover: root.noHover
opacity: 1 opacity: 1
smooth: false smooth: false
radius: root.radius radius: root.radius
@ -32,4 +34,4 @@ Rectangle {
source: "https://ipfs.infura.io/ipfs/" + thumbnail source: "https://ipfs.infura.io/ipfs/" + thumbnail
onClicked: root.clicked() onClicked: root.clicked()
} }
} }

View File

@ -14,14 +14,26 @@ Item {
property int identiconSize: 40 property int identiconSize: 40
property bool isCompact: false property bool isCompact: false
property string profileImage: chatType === Constants.chatTypeOneToOne ? chatView.getProfileImage(chatId) || "" : ""
height: 48 height: 48
width: nameAndInfo.width + chatIdenticon.width + Style.current.smallPadding width: nameAndInfo.width + chatIdenticon.width + Style.current.smallPadding
Connections {
enabled: chatType === Constants.chatTypeOneToOne
target: profileModel.contacts.list
onContactChanged: {
if (pubkey === root.chatId) {
root.profileImage = chatView.getProfileImage(root.chatId)
}
}
}
StatusIdenticon { StatusIdenticon {
id: chatIdenticon id: chatIdenticon
chatType: root.chatType chatType: root.chatType
chatName: root.chatName chatName: root.chatName
identicon: root.identicon identicon: root.profileImage || root.identicon
width: root.isCompact ? 20 : root.identiconSize width: root.isCompact ? 20 : root.identiconSize
height: root.isCompact ? 20 : root.identiconSize height: root.isCompact ? 20 : root.identiconSize
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter

View File

@ -24,7 +24,12 @@ Button {
chatId: control.chatId chatId: control.chatId
chatName: control.chatName chatName: control.chatName
chatType: control.chatType chatType: control.chatType
identicon: control.identicon identicon: {
if (control.chatType === Constants.chatTypeOneToOne) {
return chatView.getProfileImage(control.chatId) || control.identicon
}
return control.identicon
}
identiconSize: control.identiconSize identiconSize: control.identiconSize
isCompact: control.isCompact isCompact: control.isCompact
} }

View File

@ -2,25 +2,14 @@ import QtQuick 2.13
import "../../imports" import "../../imports"
import "../../shared" import "../../shared"
Rectangle { RoundedImage {
id: root id: root
property url source:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAg0lEQVR4nOzXwQmAMBAFURV7sQybsgybsgyr0QYUlE1g+Mw7ioQMe9lMQwhDaAyhMYTGEJqYkPnrj/t5XE/ft2UdW1yken7MRAyhMYTGEBpDaAyhKe9JbzvSX9WdLWYihtAYQuMLkcYQGkPUScxEDKExhMYQGkNoDKExhMYQmjsAAP//ZfIUZgXTZXQAAAAASUVORK5CYII=" noHover: true
source:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAg0lEQVR4nOzXwQmAMBAFURV7sQybsgybsgyr0QYUlE1g+Mw7ioQMe9lMQwhDaAyhMYTGEJqYkPnrj/t5XE/ft2UdW1yken7MRAyhMYTGEBpDaAyhKe9JbzvSX9WdLWYihtAYQuMLkcYQGkPUScxEDKExhMYQGkNoDKExhMYQmjsAAP//ZfIUZgXTZXQAAAAASUVORK5CYII="
width: 40 width: 40
height: 40 height: 40
color: Style.current.background
radius: width / 2
border.width: 1 border.width: 1
border.color: Style.current.borderSecondary border.color: Style.current.borderSecondary
smooth: false
Image { antialiasing: true
width: parent.width
height: parent.height
fillMode: Image.PreserveAspectFit
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
source: root.source
mipmap: true
smooth: false
antialiasing: true
}
} }

2
vendor/DOtherSide vendored

@ -1 +1 @@
Subproject commit a474e0e1fedd213fa9590811faca198761660857 Subproject commit 5c34c8e3563b2d10a2f3d44ac0ee11da13f9a916

2
vendor/nimqml vendored

@ -1 +1 @@
Subproject commit b1ba8cba3c75de574046071de824031cde6e580b Subproject commit db7042d4902dc19e5a286416ffa293e43b6249c5

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit d8307a60cfc49998a48dcb7026b3a1ca17d55985 Subproject commit 55a08e9e4d259d77637420ec318cfbafeb8f9f6e