From 847eb2623faeff9af4a07222a933757606df5545 Mon Sep 17 00:00:00 2001 From: emizzle Date: Wed, 24 Jun 2020 13:23:49 +1000 Subject: [PATCH] feat: add sticker popup Add sticker popup Add send sticker message Add ability to select sticker pack and show stickers for pack with scroll 1. Sticker history 2. Install sticker packs 3. Sticker market 1. Sticker packs are installed on app start up until installation of sticker pack functionality is added 2. Optimisations such as preloading images to be done so that sticker images are not downloaded each time. --- src/app/chat/core.nim | 9 + src/app/chat/view.nim | 43 +++- src/app/chat/views/sticker_list.nim | 43 ++++ src/app/chat/views/sticker_pack_list.nim | 60 ++++++ src/app/profile/core.nim | 4 +- src/status/chat.nim | 21 +- src/status/libstatus/chat.nim | 17 +- src/status/libstatus/contracts.nim | 21 +- src/status/libstatus/settings.nim | 14 +- src/status/libstatus/stickers.nim | 49 +++++ src/status/libstatus/types.nim | 22 ++ src/status/wallet.nim | 2 +- src/status/wallet/collectibles.nim | 3 +- ui/app/AppLayouts/Chat/ChatColumn.qml | 17 +- .../Chat/ChatColumn/ChatButtons.qml | 73 +++++++ .../AppLayouts/Chat/ChatColumn/ChatInput.qml | 76 +++---- ui/app/AppLayouts/Chat/ChatColumn/qmldir | 3 +- .../Chat/ChatColumn/samples/StickerData.qml | 66 ++++++ .../ChatColumn/samples/StickerPackData.qml | 32 +++ .../AppLayouts/Chat/ChatColumn/samples/qmldir | 2 + ui/app/AppLayouts/Chat/ContactsColumn.qml | 5 + .../Chat/ContactsColumn/AddChat.qml | 120 +++-------- ui/app/AppLayouts/Chat/components/Contact.qml | 2 +- .../Chat/components/StickersPopup.qml | 192 ++++++++++++++++++ ui/app/AppLayouts/Chat/components/qmldir | 3 +- .../Profile/Sections/Contacts/Contact.qml | 2 +- .../Wallet/components/AddAccount.qml | 157 +++++--------- ui/app/img/history_icon.svg | 3 + ui/app/img/stickers_icon.svg | 4 + ui/app/img/stickers_icon_open.svg | 4 + ui/app/img/stickers_sad_icon.svg | 4 + ui/nim-status-client.pro | 15 +- ui/onboarding/Login.qml | 2 +- ui/onboarding/Login/AddressView.qml | 2 +- ui/shared/AddButton.qml | 80 ++++++++ ui/shared/{RoundImage.qml => Identicon.qml} | 0 ui/shared/RoundedIcon.qml | 14 +- ui/shared/RoundedImage.qml | 38 ++++ ui/shared/qmldir | 3 +- 39 files changed, 942 insertions(+), 285 deletions(-) create mode 100644 src/app/chat/views/sticker_list.nim create mode 100644 src/app/chat/views/sticker_pack_list.nim create mode 100644 src/status/libstatus/stickers.nim create mode 100644 ui/app/AppLayouts/Chat/ChatColumn/ChatButtons.qml create mode 100644 ui/app/AppLayouts/Chat/ChatColumn/samples/StickerData.qml create mode 100644 ui/app/AppLayouts/Chat/ChatColumn/samples/StickerPackData.qml create mode 100644 ui/app/AppLayouts/Chat/components/StickersPopup.qml create mode 100644 ui/app/img/history_icon.svg create mode 100644 ui/app/img/stickers_icon.svg create mode 100644 ui/app/img/stickers_icon_open.svg create mode 100644 ui/app/img/stickers_sad_icon.svg create mode 100644 ui/shared/AddButton.qml rename ui/shared/{RoundImage.qml => Identicon.qml} (100%) create mode 100644 ui/shared/RoundedImage.qml diff --git a/src/app/chat/core.nim b/src/app/chat/core.nim index e411160af1..2616e18c9f 100644 --- a/src/app/chat/core.nim +++ b/src/app/chat/core.nim @@ -3,9 +3,12 @@ import ../../status/chat as chat_model import ../../status/mailservers as mailserver_model import ../../signals/types import ../../status/libstatus/types as status_types +import ../../status/libstatus/wallet as status_wallet import ../../status/[chat, contacts, status] import view, views/channels_list +from eth/common/utils import parseAddress + logScope: topics = "chat-controller" @@ -70,6 +73,12 @@ proc init*(self: ChatController) = self.status.mailservers.init() self.status.chat.init() + let currAcct = status_wallet.getWalletAccounts()[0] # TODO: make generic + let currAddr = parseAddress(currAcct.address) + let installedStickers = self.status.chat.getInstalledStickers(currAddr) + for stickerPack in installedStickers: + self.view.addStickerPackToList(stickerPack) + proc handleMessage(self: ChatController, data: MessageSignal) = self.status.chat.update(data.chats, data.messages) diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index b46cab8a1f..6873ca0866 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -4,9 +4,10 @@ import ../../status/status import ../../status/chat as status_chat import ../../status/contacts as status_contacts import ../../status/chat/[chat, message] +import ../../status/libstatus/types import ../../status/profile/profile -import views/channels_list, views/message_list, views/chat_item +import views/channels_list, views/message_list, views/chat_item, views/sticker_pack_list, views/sticker_list logScope: topics = "chats-view" @@ -19,6 +20,10 @@ QtObject: callResult: string messageList: Table[string, ChatMessageList] activeChannel*: ChatItemView + activeStickerPackId*: int + stickerPacks*: StickerPackList + stickers*: Table[int, StickerList] + emptyStickerList: StickerList proc setup(self: ChatsView) = self.QAbstractListModel.setup @@ -28,6 +33,7 @@ QtObject: for msg in self.messageList.values: msg.delete self.messageList = initTable[string, ChatMessageList]() + self.stickers = initTable[int, StickerList]() self.QAbstractListModel.delete proc newChatsView*(status: Status): ChatsView = @@ -35,9 +41,23 @@ QtObject: result.status = status result.chats = newChannelsList() result.activeChannel = newChatItemView(status) + result.activeStickerPackId = -1 result.messageList = initTable[string, ChatMessageList]() + result.stickerPacks = newStickerPackList() + result.stickers = initTable[int, StickerList]() + result.emptyStickerList = newStickerList() result.setup() + proc addStickerPackToList*(self: ChatsView, stickerPack: StickerPack) = + discard self.stickerPacks.addStickerPackToList(stickerPack) + self.stickers[stickerPack.id] = newStickerList(stickerPack.stickers) + + proc getStickerPackList(self: ChatsView): QVariant {.slot.} = + newQVariant(self.stickerPacks) + + QtProperty[QVariant] stickerPacks: + read = getStickerPackList + proc getChatsList(self: ChatsView): QVariant {.slot.} = newQVariant(self.chats) @@ -67,6 +87,24 @@ QtObject: read = getActiveChannelIdx write = setActiveChannelByIndex notify = activeChannelChanged + + proc activeStickerPackChanged*(self: ChatsView) {.signal.} + + proc setActiveStickerPackById*(self: ChatsView, id: int) {.slot.} = + if self.activeStickerPackId == id: + return + + self.activeStickerPackId = id + self.activeStickerPackChanged() + + proc getStickerList*(self: ChatsView): QVariant {.slot.} = + if self.activeStickerPackId <= 0: + return newQVariant(self.emptyStickerList) + result = newQVariant(self.stickers[self.activeStickerPackId]) + + QtProperty[QVariant] stickers: + read = getStickerList + notify = activeStickerPackChanged proc setActiveChannel*(self: ChatsView, channel: string) = if(channel == ""): return @@ -120,6 +158,9 @@ QtObject: proc sendMessage*(self: ChatsView, message: string) {.slot.} = discard self.status.chat.sendMessage(self.activeChannel.id, message) + + proc sendSticker*(self: ChatsView, hash: string, pack: int) {.slot.} = + self.status.chat.sendSticker(self.activeChannel.id, hash, pack) proc joinChat*(self: ChatsView, channel: string, chatTypeInt: int): int {.slot.} = self.status.chat.join(channel, ChatType(chatTypeInt)) diff --git a/src/app/chat/views/sticker_list.nim b/src/app/chat/views/sticker_list.nim new file mode 100644 index 0000000000..86643ce855 --- /dev/null +++ b/src/app/chat/views/sticker_list.nim @@ -0,0 +1,43 @@ +import NimQml, Tables +import ../../../status/chat/stickers + +import ../../../status/libstatus/types + +type + StickerRoles {.pure.} = enum + Url = UserRole + 1 + Hash = UserRole + 2 + +QtObject: + type + StickerList* = ref object of QAbstractListModel + stickers*: seq[Sticker] + + proc setup(self: StickerList) = self.QAbstractListModel.setup + + proc delete(self: StickerList) = self.QAbstractListModel.delete + + proc newStickerList*(stickers: seq[Sticker] = @[]): StickerList = + new(result, delete) + result.stickers = stickers + result.setup() + + method rowCount(self: StickerList, index: QModelIndex = nil): int = self.stickers.len + + method data(self: StickerList, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.stickers.len: + return + + let sticker = self.stickers[index.row] + let stickerRole = role.StickerRoles + case stickerRole: + of StickerRoles.Url: result = newQVariant(decodeContentHash(sticker.hash)) + of StickerRoles.Hash: result = newQVariant(sticker.hash) + + method roleNames(self: StickerList): Table[int, string] = + { + StickerRoles.Url.int:"url", + StickerRoles.Hash.int:"hash" + }.toTable diff --git a/src/app/chat/views/sticker_pack_list.nim b/src/app/chat/views/sticker_pack_list.nim new file mode 100644 index 0000000000..d594120831 --- /dev/null +++ b/src/app/chat/views/sticker_pack_list.nim @@ -0,0 +1,60 @@ +import NimQml, Tables +import ../../../status/chat/stickers +import ../../../status/libstatus/types + +type + StickerPackRoles {.pure.} = enum + Author = UserRole + 1, + Id = UserRole + 2 + Name = UserRole + 3 + Price = UserRole + 4 + Preview = UserRole + 5 + Thumbnail = UserRole + 6 + +QtObject: + type + StickerPackList* = ref object of QAbstractListModel + packs*: seq[StickerPack] + + proc setup(self: StickerPackList) = self.QAbstractListModel.setup + + proc delete(self: StickerPackList) = self.QAbstractListModel.delete + + proc newStickerPackList*(): StickerPackList = + new(result, delete) + result.packs = @[] + result.setup() + + method rowCount(self: StickerPackList, index: QModelIndex = nil): int = self.packs.len + + method data(self: StickerPackList, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.packs.len: + return + + let stickerPack = self.packs[index.row] + let stickerPackRole = role.StickerPackRoles + case stickerPackRole: + of StickerPackRoles.Author: result = newQVariant(stickerPack.author) + of StickerPackRoles.Id: result = newQVariant($stickerPack.id) + of StickerPackRoles.Name: result = newQVariant(stickerPack.name) + of StickerPackRoles.Price: result = newQVariant(stickerPack.price) + of StickerPackRoles.Preview: result = newQVariant(decodeContentHash(stickerPack.preview)) + of StickerPackRoles.Thumbnail: result = newQVariant(decodeContentHash(stickerPack.thumbnail)) + + method roleNames(self: StickerPackList): Table[int, string] = + { + StickerPackRoles.Author.int:"author", + StickerPackRoles.Id.int:"id", + StickerPackRoles.Name.int: "name", + StickerPackRoles.Price.int: "price", + StickerPackRoles.Preview.int: "preview", + StickerPackRoles.Thumbnail.int: "thumbnail" + }.toTable + + proc addStickerPackToList*(self: StickerPackList, pack: StickerPack): int = + self.beginInsertRows(newQModelIndex(), 0, 0) + self.packs.insert(pack, 0) + self.endInsertRows() + result = 0 diff --git a/src/app/profile/core.nim b/src/app/profile/core.nim index 34044bf91b..9a1dec705f 100644 --- a/src/app/profile/core.nim +++ b/src/app/profile/core.nim @@ -31,8 +31,8 @@ proc init*(self: ProfileController, account: Account) = # Ideally, this module should call getSettings once, and fill the # profile with all the information comming from the settings. let response = status_settings.getSettings() - let pubKey = parseJSON($response)["result"]["public-key"].getStr - let mnemonic = parseJSON($response)["result"]["mnemonic"].getStr + let pubKey = response["public-key"].getStr + let mnemonic = response["mnemonic"].getStr profile.id = pubKey self.view.setNewProfile(profile) diff --git a/src/status/chat.nim b/src/status/chat.nim index 55df43318a..78b9c1e2e9 100644 --- a/src/status/chat.nim +++ b/src/status/chat.nim @@ -1,9 +1,12 @@ import eventemitter, json, strutils, sequtils, tables, chronicles import libstatus/chat as status_chat +import libstatus/stickers as status_stickers +import libstatus/types import profile/profile -import chat/[chat, message] +import chat/[chat, message], wallet import ../signals/messages import ens +import eth/common/eth_types type ChatUpdateArgs* = ref object of Args @@ -78,9 +81,21 @@ proc join*(self: ChatModel, chatId: string, chatType: ChatType) = self.events.emit("channelJoined", ChannelArgs(chat: chat)) +proc getInstalledStickers*(self: ChatModel, address: EthAddress): seq[StickerPack] = + # TODO: needs more fleshing out to determine which sticker packs + # we own -- owned sticker packs will simply allowed them to be installed + discard status_stickers.getBalance(address) + + result = status_stickers.getInstalledStickers() + proc init*(self: ChatModel) = let chatList = status_chat.loadChats() + # TODO: Temporarily install sticker packs as a first step. Later, once installation + # of sticker packs is supported, this should be removed, and a default "No + # stickers installed" view should show if no sticker packs are installed. + status_stickers.installStickers() + var filters:seq[JsonNode] = @[] for chat in chatList: if self.hasChannel(chat.id): continue @@ -133,6 +148,10 @@ proc sendMessage*(self: ChatModel, chatId: string, msg: string): string = self.emitUpdate(sentMessage) sentMessage +proc sendSticker*(self: ChatModel, chatId: string, hash: string, pack: int) = + var response = status_chat.sendStickerMessage(chatId, hash, pack) + self.emitUpdate(response) + proc chatMessages*(self: ChatModel, chatId: string, initialLoad:bool = true) = if not self.msgCursor.hasKey(chatId): self.msgCursor[chatId] = ""; diff --git a/src/status/libstatus/chat.nim b/src/status/libstatus/chat.nim index 900ff95852..9d3ccfdf59 100644 --- a/src/status/libstatus/chat.nim +++ b/src/status/libstatus/chat.nim @@ -78,7 +78,22 @@ proc sendChatMessage*(chatId: string, msg: string): string = "responseTo": nil, "ensName": nil, "sticker": nil, - "contentType": 1 + "contentType": ContentType.Message.int + } + ]) + +proc sendStickerMessage*(chatId: string, hash: string, pack: int): string = + callPrivateRPC("sendChatMessage".prefix, %* [ + { + "chatId": chatId, + "text": "Update to latest version to see a nice sticker here!", + "responseTo": nil, + "ensName": nil, + "sticker": { + "hash": hash, + "pack": pack + }, + "contentType": ContentType.Sticker.int } ]) diff --git a/src/status/libstatus/contracts.nim b/src/status/libstatus/contracts.nim index c4f6bdd4c4..43400bbc51 100644 --- a/src/status/libstatus/contracts.nim +++ b/src/status/libstatus/contracts.nim @@ -1,4 +1,5 @@ -import sequtils, strformat, sugar, macros, tables, eth/common/eth_types, stew/byteutils, nimcrypto +import sequtils, strformat, sugar, macros, tables +import eth/common/eth_types, stew/byteutils, nimcrypto from eth/common/utils import parseAddress type @@ -18,14 +19,26 @@ type Contract* = ref object methods*: Table[string, Method] let CONTRACTS: seq[Contract] = @[ - Contract(name: "snt", network: Network.Mainnet, address: parseAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e")), + Contract(name: "snt", network: Network.Mainnet, address: parseAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e"), + methods: [ + ("approveAndCall", Method(signature: "approveAndCall(address,uint256,bytes)")) + ].toTable + ), Contract(name: "snt", network: Network.Testnet, address: parseAddress("0xc55cf4b03948d7ebc8b9e8bad92643703811d162")), Contract(name: "tribute-to-talk", network: Network.Testnet, address: parseAddress("0xC61aa0287247a0398589a66fCD6146EC0F295432")), Contract(name: "stickers", network: Network.Mainnet, address: parseAddress("0x0577215622f43a39f4bc9640806dfea9b10d2a36")), Contract(name: "stickers", network: Network.Testnet, address: parseAddress("0x8cc272396be7583c65bee82cd7b743c69a87287d")), - Contract(name: "sticker-market", network: Network.Mainnet, address: parseAddress("0x12824271339304d3a9f7e096e62a2a7e73b4a7e7")), + Contract(name: "sticker-market", network: Network.Mainnet, address: parseAddress("0x12824271339304d3a9f7e096e62a2a7e73b4a7e7"), + methods: [ + ("buyToken", Method(signature: "buyToken(uint256,address,uint256)")) + ].toTable + ), Contract(name: "sticker-market", network: Network.Testnet, address: parseAddress("0x6CC7274aF9cE9572d22DFD8545Fb8c9C9Bcb48AD")), - Contract(name: "sticker-pack", network: Network.Mainnet, address: parseAddress("0x110101156e8F0743948B2A61aFcf3994A8Fb172e")), + Contract(name: "sticker-pack", network: Network.Mainnet, address: parseAddress("0x110101156e8F0743948B2A61aFcf3994A8Fb172e"), + methods: [ + ("balanceOf", Method(signature: "balanceOf(address)")) + ].toTable + ), Contract(name: "sticker-pack", network: Network.Testnet, address: parseAddress("0xf852198d0385c4b871e0b91804ecd47c6ba97351")), # Strikers seems dead. Their website doesn't work anymore Contract(name: "strikers", network: Network.Mainnet, address: parseAddress("0xdcaad9fd9a74144d226dbf94ce6162ca9f09ed7e"), diff --git a/src/status/libstatus/settings.nim b/src/status/libstatus/settings.nim index 1f44880707..40081fc007 100644 --- a/src/status/libstatus/settings.nim +++ b/src/status/libstatus/settings.nim @@ -1,14 +1,18 @@ -import json import core +import json proc saveSettings*(key: string, value: string): string = callPrivateRPC("settings_saveSetting", %* [ key, $value ]) -proc getSettings*(): string = - callPrivateRPC("settings_getSettings") -# TODO: return an Table/Object instead of string - proc getWeb3ClientVersion*(): string = parseJson(callPrivateRPC("web3_clientVersion"))["result"].getStr + +proc getSettings*(): JsonNode = + callPrivateRPC("settings_getSettings").parseJSON()["result"] + # TODO: return an Table/Object instead + +proc getSetting*(name: string): string = + let settings: JsonNode = getSettings() + result = settings{name}.getStr diff --git a/src/status/libstatus/stickers.nim b/src/status/libstatus/stickers.nim new file mode 100644 index 0000000000..1ff0ac8c81 --- /dev/null +++ b/src/status/libstatus/stickers.nim @@ -0,0 +1,49 @@ +import eth/common/eth_types +import ./core as status, ./types, ./contracts, ./settings, ./utils +import json, json_serialization, tables, chronicles, strutils + +# Retrieves number of sticker packs owned by user +# See https://notes.status.im/Q-sQmQbpTOOWCQcYiXtf5g#Read-Sticker-Packs-owned-by-a-user +# for more details +proc getBalance*(address: EthAddress): int = + let contract = contracts.getContract(Network.Mainnet, "sticker-pack") + let payload = %* [{ + "to": $contract.address, + "data": contract.methods["balanceOf"].encodeAbi(address) + }, "latest"] + + let responseStr = status.callPrivateRPC("eth_call", payload) + let response = Json.decode(responseStr, RpcResponse) + if response.error != "": + raise newException(RpcException, "Error getting stickers balance: " & response.error) + result = fromHex[int](response.result) + +# Buys a sticker pack for user +# See https://notes.status.im/Q-sQmQbpTOOWCQcYiXtf5g#Buy-a-Sticker-Pack for more +# details +proc buyPack*(packId: int, address: EthAddress, price: int, password: string): string = + let stickerMktContract = contracts.getContract(Network.Mainnet, "sticker-market") + let sntContract = contracts.getContract(Network.Mainnet, "sticker-market") + let buyTxAbiEncoded = stickerMktContract.methods["buyToken"].encodeAbi(packId, address, price) + let approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi($stickerMktContract.address, price, buyTxAbiEncoded) + let payload = %* [{ + "from": $address, + "to": $sntContract.address, + "gas": 200000, + "data": approveAndCallAbiEncoded + }, "latest"] + + let responseStr = status.sendTransaction($payload, password) + let response = Json.decode(responseStr, RpcResponse) + if response.error != "": + raise newException(RpcException, "Error getting stickers balance: " & response.error) + result = response.result # should be a tx receipt + +proc installStickers*() = + discard settings.saveSettings("stickers/packs-installed", """{"1":{"author":"cryptoworld1373","id":1,"name":"Status Cat","preview":"e3010170122050efc0a3e661339f31e1e44b3d15a1bf4e501c965a0523f57b701667fa90ccca","price":0,"stickers":[{"hash":"e30101701220eab9a8ef4eac6c3e5836a3768d8e04935c10c67d9a700436a0e53199e9b64d29"},{"hash":"e30101701220c8f28aebe4dbbcee896d1cdff89ceeaceaf9f837df55c79125388f954ee5f1fe"},{"hash":"e301017012204861f93e29dd8e7cf6699135c7b13af1bce8ceeaa1d9959ab8592aa20f05d15f"},{"hash":"e301017012203ffa57a51cceaf2ce040852de3b300d395d5ba4d70e08ba993f93a25a387e3a9"},{"hash":"e301017012204f2674db0bc7f7cfc0382d1d7f79b4ff73c41f5c487ef4c3bb3f3a4cf3f87d70"},{"hash":"e30101701220e8d4d8b9fb5f805add2f63c1cb5c891e60f9929fc404e3bb725aa81628b97b5f"},{"hash":"e301017012206fdad56fe7a2facb02dabe8294f3ac051443fcc52d67c2fbd8615eb72f9d74bd"},{"hash":"e30101701220a691193cf0559905c10a3c5affb9855d730eae05509d503d71327e6c820aaf98"},{"hash":"e30101701220d8004af925f8e85b4e24813eaa5ef943fa6a0c76035491b64fbd2e632a5cc2fd"},{"hash":"e3010170122049f7bc650615568f14ee1cfa9ceaf89bfbc4745035479a7d8edee9b4465e64de"},{"hash":"e301017012201915dc0faad8e6783aca084a854c03553450efdabf977d57b4f22f73d5c53b50"},{"hash":"e301017012200b9fb71a129048c2a569433efc8e4d9155c54d598538be7f65ea26f665be1e84"},{"hash":"e30101701220d37944e3fb05213d45416fa634cf9e10ec1f43d3bf72c4eb3062ae6cc4ed9b08"},{"hash":"e3010170122059390dca66ba8713a9c323925bf768612f7dd16298c13a07a6b47cb5af4236e6"},{"hash":"e30101701220daaf88ace8a3356559be5d6912d5d442916e3cc92664954526c9815d693dc32b"},{"hash":"e301017012203ae30594fdf56d7bfd686cef1a45c201024e9c10a792722ef07ba968c83c064d"},{"hash":"e3010170122016e5eba0bbd32fc1ff17d80d1247fc67432705cd85731458b52febb84fdd6408"},{"hash":"e3010170122014fe2c2186cbf9d15ff61e04054fd6b0a5dbd7f365a1807f6f3d3d3e93e50875"},{"hash":"e30101701220f23a7dad3ea7ad3f3553a98fb305148d285e4ebf66b427d85a2340f66d51da94"},{"hash":"e3010170122047a637c6af02904a8ae702ec74b3df5fd8914df6fb11c99446a36d890beeb7ee"},{"hash":"e30101701220776f1ff89f6196ae68414545f6c6a5314c35eee7406cb8591d607a2b0533cc86"}],"thumbnail":"e30101701220e9876531554a7cb4f20d7ebbf9daef2253e6734ad9c96ba288586a9b88bef491"},"2":{"author":"ETHDenver","id":2,"name":"ETHDenver Bufficorn","preview":"e30101701220a62487ef23b1bbdc2bf39583bb4259bda032450ac90d199eec8b0b74fe8de580","price":0,"stickers":[{"hash":"e301017012209cba61faaa78931ddee49461ab1301a88a034f077ced9de6351126470b80fe32"},{"hash":"e30101701220e8109e8f4bf398a252bfb8d88fe44554c502c5ab8025e7e1abba692028333a97"},{"hash":"e30101701220079952b304013fe6bcd5ac00a7480e467badece24a19b00ddea64635d9d5ecae"},{"hash":"e301017012200a21dc7c6ef96cdb3f9b53ed1fdea4ee22baa8264a208c3705a8836b664efd8d"},{"hash":"e30101701220b3ab61589851545f2c3ce031beafbf808070c36e6abbaf8c7c8076458c3f06e0"},{"hash":"e30101701220a5274e0415793e1a81347f594715d6006538d78819a731324d3fec19ab762deb"},{"hash":"e3010170122076ae7e71383586ecfa0a6f9cc8af80e614ae466facabe48bee167d2921dd0420"},{"hash":"e30101701220bfe0dd7e214eac7cb75c7e8f6e9b1d02732e01ffc67a460cf3b1311aed92a2e9"},{"hash":"e3010170122042aa1a5e8dc25072ee6d0b43535701fcb44c58b18e6c14a4956a3212f64cf236"},{"hash":"e30101701220107d2ca1901cd8ac0ce54cbf93f5ac86d931c5d4bb16d402315a2a8197dac371"},{"hash":"e301017012206d2d66f7f2fff9f366e910f4d157f7ac59dead3e42b2f9fbe5a9b95aea146d10"},{"hash":"e301017012203a86612e82ea0db8248875e7993e73a65ee263f6bbeb2b1738eb12a6ec572965"},{"hash":"e301017012205bc05fe6517cc00e95e34ba978dd2a5cee91e4dc4efceb4505fecc38887bb7fb"},{"hash":"e301017012203cea2a96032284de80587e994c669a42405d49ce599281866ccc14f475e7870b"}],"thumbnail":"e30101701220d06f13f3de8da081ef2a1bc36ffa283c1bfe093bf45bc0332a6d748196e8ce16"},"5":{"name":"Ghostatus","author":"Brooklyn Design Factory","thumbnail":"e30101701220a7beb4be086ad31ae19c64e5a832853571e239d9799a923a03779c4435c6fdad","preview":"e3010170122027c67c9acbe98786f6db4aabca3fd3ec04993eaa3e08811aefe27d9786c3bf00","stickers":[{"hash":"e30101701220fff8527a1b37070d46c9077877b7f7cc74da5c31adafe77ba65e5efefebf5d91"},{"hash":"e301017012208023d8c6bd327b0ac2be66423d59776a753d5f5492975fe0bd5b5601d7c1d9d3"},{"hash":"e3010170122064f4e8fa00a5b8164689e038a4d74e0b12f4490dcd4112e80057c254f6fbc135"},{"hash":"e301017012200d50bd618b0aed0562ed153de0bf77da766646e81a848982a2f8aaf7d7e94dcc"},{"hash":"e3010170122055f08854a40acaac60355d9bb3eaa730b994e2e13484e67d2675103e0cda0c88"},{"hash":"e301017012203fc2acfed328918bf000ee637ab4c25fa38f2c69b378b69b9212d61747d30c02"},{"hash":"e3010170122096930b99e08c6c28c88c0b74bae7a0159f5c6438ab7d50294987533dabfee863"},{"hash":"e3010170122051ddbe29bee4bbc5fcf50d81faad0872f32b88cea4e4e4fcdbf2daf5d09eda76"},{"hash":"e301017012200647e07651c163515ce34d18b3c8636eeb4798dbaa1766b2a60facc59999b261"},{"hash":"e30101701220c539bfa744e39cf2ece1ab379a15c95338d513a9ce5178d4ad28be486b801bc2"},{"hash":"e301017012205ea333b9eb89918ed592f43372bd58dc3a91a7a71aa68b37369c2f66f931fd87"},{"hash":"e3010170122007f05ba31bd77003bff562ed932a8b440de1ad05481dc622b1c0c571d6b39ffc"},{"hash":"e30101701220906b7a664a87707db72921cf5c7416c61a717dfcb5fcff9bc04b28c612ae554d"}],"id":5,"price":0}}""") + +proc getInstalledStickers*(): seq[StickerPack] = + let setting = settings.getSetting("stickers/packs-installed").parseJson + result = newSeq[StickerPack]() + for i in setting.keys: + result.add(Json.decode($(setting[i]), StickerPack)) diff --git a/src/status/libstatus/types.nim b/src/status/libstatus/types.nim index 09d178a45e..382aedb23b 100644 --- a/src/status/libstatus/types.nim +++ b/src/status/libstatus/types.nim @@ -63,6 +63,13 @@ type keyUid*: string photoPath*: string +type + RpcResponse* = ref object + jsonrpc*: string + result*: string + id*: int + error*: string + proc toAccount*(account: GeneratedAccount): Account = result = Account(name: account.name, photoPath: account.photoPath, keyUid: account.address) @@ -90,3 +97,18 @@ type value*: string fromAddress*: string to*: string + +type + RpcException* = object of Exception + +type Sticker* = ref object + hash*: string + +type StickerPack* = ref object + author*: string + id*: int + name*: string + price*: int + preview*: string + stickers*: seq[Sticker] + thumbnail*: string diff --git a/src/status/wallet.nim b/src/status/wallet.nim index d9b67da000..1efaf0d7fd 100644 --- a/src/status/wallet.nim +++ b/src/status/wallet.nim @@ -50,7 +50,7 @@ proc sendTransaction*(self: WalletModel, from_value: string, to: string, value: proc getDefaultCurrency*(self: WalletModel): string = # TODO: this should come from a model? It is going to be used too in the # profile section and ideally we should not call the settings more than once - status_settings.getSettings().parseJSON()["result"]["currency"].getStr + status_settings.getSetting("currency") proc setDefaultCurrency*(self: WalletModel, currency: string) = discard status_settings.saveSettings("currency", currency) diff --git a/src/status/wallet/collectibles.nim b/src/status/wallet/collectibles.nim index 81f0c770d4..d6a4078c7b 100644 --- a/src/status/wallet/collectibles.nim +++ b/src/status/wallet/collectibles.nim @@ -1,5 +1,6 @@ import strformat, httpclient, json, chronicles, sequtils, strutils, tables -import ../libstatus/[core, contracts] +import ../libstatus/core as status +import ../libstatus/contracts as contracts import eth/common/eth_types import account diff --git a/ui/app/AppLayouts/Chat/ChatColumn.qml b/ui/app/AppLayouts/Chat/ChatColumn.qml index ece016573c..92f478ca9f 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn.qml @@ -36,15 +36,24 @@ StackLayout { } } - RowLayout { + + + Rectangle { id: chatInputContainer height: 70 + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom Layout.fillWidth: true - Layout.bottomMargin: 0 - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + Layout.preferredWidth: parent.width + Layout.preferredHeight: height transformOrigin: Item.Bottom + clip: true - ChatInput {} + ChatInput { + anchors.fill: parent + anchors.leftMargin: -border.width + border.width: 1 + border.color: Theme.grey + } } } diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ChatButtons.qml b/ui/app/AppLayouts/Chat/ChatColumn/ChatButtons.qml new file mode 100644 index 0000000000..bea917af65 --- /dev/null +++ b/ui/app/AppLayouts/Chat/ChatColumn/ChatButtons.qml @@ -0,0 +1,73 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import "../../../../imports" +import "../components" + +Rectangle { + border.width: 0 + + Button { + id: chatSendBtn + visible: txtData.length > 0 + width: 30 + height: 30 + text: "" + anchors.rightMargin: Theme.padding + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + onClicked: { + chatsModel.sendMessage(txtData.text) + txtData.text = "" + } + background: Rectangle { + color: parent.enabled ? Theme.blue : Theme.grey + radius: 50 + } + Image { + source: "../../../img/arrowUp.svg" + width: 12 + fillMode: Image.PreserveAspectFit + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Image { + id: stickersIcon + visible: txtData.length == 0 + width: 20 + height: 20 + anchors.rightMargin: Theme.padding + anchors.right: parent.right + fillMode: Image.PreserveAspectFit + source: "../../../img/stickers_icon" + (stickersPopup.opened ? "_open.svg" : ".svg") + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + onClicked: { + if (stickersPopup.opened) { + stickersPopup.close() + } else { + stickersPopup.open() + } + } + } + } + + StickersPopup { + id: stickersPopup + width: 360 + height: 440 + x: parent.width - width - 8 + y: parent.height - sendBtns.height - height - 8 + stickerList: chatsModel.stickers + stickerPackList: chatsModel.stickerPacks + } +} +/*##^## +Designer { + D{i:0;autoSize:true;height:480;width:640} +} +##^##*/ diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ChatInput.qml b/ui/app/AppLayouts/Chat/ChatColumn/ChatInput.qml index c670e68a86..52509c645a 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/ChatInput.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/ChatInput.qml @@ -1,67 +1,31 @@ import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 +import "../components" import "../../../../shared" import "../../../../imports" Rectangle { - id: element2 - width: 200 - height: 70 - Layout.fillWidth: true - color: "white" border.width: 0 visible: chatsModel.activeChannel.chatType != Constants.chatTypePrivateGroupChat || chatsModel.activeChannel.isMember(profileModel.profile.pubKey) - Rectangle { - id: rectangle - color: "#00000000" - border.color: Theme.grey + RowLayout { + spacing: 0 anchors.fill: parent - Button { - id: chatSendBtn - visible: txtData.length > 0 - x: 100 - width: 30 - height: 30 - text: "" - anchors.top: parent.top - anchors.topMargin: 20 - anchors.right: parent.right - anchors.rightMargin: 16 - onClicked: { - chatsModel.sendMessage(txtData.text) - txtData.text = "" - } - background: Rectangle { - color: parent.enabled ? Theme.blue : Theme.grey - radius: 50 - } - Image { - source: "../../../img/arrowUp.svg" - width: 12 - fillMode: Image.PreserveAspectFit - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - StyledTextField { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: parent.width - sendBtns.width + id: txtData text: "" - padding: 0 + leftPadding: 12 + rightPadding: Theme.padding font.pixelSize: 14 placeholderText: qsTr("Type a message...") - anchors.right: chatSendBtn.left - anchors.rightMargin: 16 - anchors.top: parent.top - anchors.topMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - anchors.left: parent.left - leftPadding: 24 + selectByMouse: true Keys.onEnterPressed: { chatsModel.sendMessage(txtData.text) @@ -75,10 +39,28 @@ Rectangle { color: "#00000000" } } + + ChatButtons { + id: sendBtns + Layout.topMargin: 1 + Layout.fillHeight: true + Layout.preferredWidth: 30 + Theme.padding + Layout.minimumWidth: 30 + Theme.padding + Layout.maximumWidth: 200 + } + } + + MouseArea { + id: mouseArea1 + anchors.rightMargin: 50 + anchors.fill: parent + onClicked: { + txtData.forceActiveFocus(Qt.MouseFocusReason) + } } } /*##^## Designer { - D{i:0;width:600} + D{i:0;autoSize:true;formeditorColor:"#ffffff";height:100;width:600} } ##^##*/ diff --git a/ui/app/AppLayouts/Chat/ChatColumn/qmldir b/ui/app/AppLayouts/Chat/ChatColumn/qmldir index b2d97eefc7..15b295fa80 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/qmldir +++ b/ui/app/AppLayouts/Chat/ChatColumn/qmldir @@ -1,4 +1,5 @@ TopBar 1.0 TopBar.qml ChatMessages 1.0 ChatMessages.qml ChatInput 1.0 ChatInput.qml -EmptyChat 1.0 EmptyChat.qml \ No newline at end of file +EmptyChat 1.0 EmptyChat.qml +ChatButtons 1.0 ChatButtons.qml \ No newline at end of file diff --git a/ui/app/AppLayouts/Chat/ChatColumn/samples/StickerData.qml b/ui/app/AppLayouts/Chat/ChatColumn/samples/StickerData.qml new file mode 100644 index 0000000000..bb5a369ac8 --- /dev/null +++ b/ui/app/AppLayouts/Chat/ChatColumn/samples/StickerData.qml @@ -0,0 +1,66 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 +import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.3 +import Qt.labs.platform 1.1 + +ListModel { + ListElement { + hash: "e30101701220fff8527a1b37070d46c9077877b7f7cc74da5c31adafe77ba65e5efefebf5d91" + url: "QmfZrHmLR5VvkXSDbArDR3TX6j4FgpDcrvNz2fHSJk1VvG" + } + ListElement { + hash: "e301017012208023d8c6bd327b0ac2be66423d59776a753d5f5492975fe0bd5b5601d7c1d9d3" + url: "QmWxrbdgU5q3VxStTQr4VTBdJNqwatMAUf8KZBLEfqZyii" + } + ListElement { + hash: "e3010170122064f4e8fa00a5b8164689e038a4d74e0b12f4490dcd4112e80057c254f6fbc135" + url: "QmV8k5Y4mE8hhiydohBw8hqRQgFzAQMiveCYKZ6PkKNVgg" + } + ListElement { + hash: "e301017012200d50bd618b0aed0562ed153de0bf77da766646e81a848982a2f8aaf7d7e94dcc" + url: "QmPEdR6ayeLouro7FfkQSt5aq3nfzYK4FT4nxnoxn5FuhV" + } + ListElement { + hash: "e3010170122055f08854a40acaac60355d9bb3eaa730b994e2e13484e67d2675103e0cda0c88" + url: "QmU886Hu3XAwYp8hfkMVhKoMNsPeeAJr7wgjYqAiAd4enB" + } + ListElement { + hash: "e301017012203fc2acfed328918bf000ee637ab4c25fa38f2c69b378b69b9212d61747d30c02" + url: "QmSdYZpjEAUG3fUpkPpC9ATnoaaiTLLj3W587uwx943AC1" + } + ListElement { + hash: "e3010170122096930b99e08c6c28c88c0b74bae7a0159f5c6438ab7d50294987533dabfee863" + url: "QmYURuqdkoycSLLz2qfMgjeK18Y4kT8PQfExRfbziVLSZ8" + } + ListElement { + hash: "e3010170122051ddbe29bee4bbc5fcf50d81faad0872f32b88cea4e4e4fcdbf2daf5d09eda76" + url: "QmTrDqsuN4DgKfjnDrWgo92EFCD1vFffYcZ1jMEr3AgCnH" + } + ListElement { + hash: "e301017012200647e07651c163515ce34d18b3c8636eeb4798dbaa1766b2a60facc59999b261" + url: "QmNmAiwa9PDcZs7b2oQ2D1wHfQfi1MFjp6QcfzHzGhhNJt" + } + ListElement { + hash: "e30101701220c539bfa744e39cf2ece1ab379a15c95338d513a9ce5178d4ad28be486b801bc2" + url: "QmbcY6hwDt3EKK6pJGJ6yFn3A872y2ajeBRrrKRj7KC3BX" + } + ListElement { + hash: "e301017012205ea333b9eb89918ed592f43372bd58dc3a91a7a71aa68b37369c2f66f931fd87" + url: "QmUi5NQSMg2kmhkLUtQ9Sjxnkwgn68x88wL2n357VAZdyx" + } + ListElement { + hash: "e3010170122007f05ba31bd77003bff562ed932a8b440de1ad05481dc622b1c0c571d6b39ffc" + url: "QmNse8urb4GJGEHYgUpRJBRwgiWaTGZtkoibVVUpveqfBq" + } + ListElement { + hash: "e30101701220906b7a664a87707db72921cf5c7416c61a717dfcb5fcff9bc04b28c612ae554d" + url: "QmY4QULmzFQ2AAbEuMvnd3Nd7qD8eWtyxiLD9CAf3kFZWU" + } +} + +/*##^## +Designer { + D{i:0;autoSize:true;height:480;width:640} +} +##^##*/ diff --git a/ui/app/AppLayouts/Chat/ChatColumn/samples/StickerPackData.qml b/ui/app/AppLayouts/Chat/ChatColumn/samples/StickerPackData.qml new file mode 100644 index 0000000000..6d5c1f793f --- /dev/null +++ b/ui/app/AppLayouts/Chat/ChatColumn/samples/StickerPackData.qml @@ -0,0 +1,32 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 + +ListModel { + ListElement { + author: "cryptoworld1373" + name: "Status Cat" + price: 0 + preview: "e3010170122050efc0a3e661339f31e1e44b3d15a1bf4e501c965a0523f57b701667fa90ccca" + thumbnail: "QmfZrHmLR5VvkXSDbArDR3TX6j4FgpDcrvNz2fHSJk1VvG" + } + ListElement { + author: "ETHDenver" + name: "ETHDenver Bufficorn" + price: 0 + preview: "e30101701220a62487ef23b1bbdc2bf39583bb4259bda032450ac90d199eec8b0b74fe8de580" + thumbnail: "e30101701220d06f13f3de8da081ef2a1bc36ffa283c1bfe093bf45bc0332a6d748196e8ce16" + } + ListElement { + author: "Brooklyn Design Factory" + name: "Ghostatus" + price: 0 + preview: "e3010170122027c67c9acbe98786f6db4aabca3fd3ec04993eaa3e08811aefe27d9786c3bf00" + thumbnail: "e30101701220a7beb4be086ad31ae19c64e5a832853571e239d9799a923a03779c4435c6fdad" + } +} + +/*##^## +Designer { + D{i:0;autoSize:true;height:480;width:640} +} +##^##*/ diff --git a/ui/app/AppLayouts/Chat/ChatColumn/samples/qmldir b/ui/app/AppLayouts/Chat/ChatColumn/samples/qmldir index eb4c9c12be..1074e3c2c2 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/samples/qmldir +++ b/ui/app/AppLayouts/Chat/ChatColumn/samples/qmldir @@ -1 +1,3 @@ MessagesData 1.0 MessagesData.qml +StickerData 1.0 StickerData.qml +StickerPackData 1.0 StickerPackData.qml diff --git a/ui/app/AppLayouts/Chat/ContactsColumn.qml b/ui/app/AppLayouts/Chat/ContactsColumn.qml index 949560ac91..515eefc117 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn.qml @@ -1,6 +1,7 @@ import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 + import "../../../imports" import "../../../shared" import "./components" @@ -50,6 +51,10 @@ Item { AddChat { id: addChat + anchors.right: parent.right + anchors.rightMargin: Theme.padding + anchors.top: parent.top + anchors.topMargin: 59 } StackLayout { diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml b/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml index 4d9f5b8a20..4d5daada6e 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml @@ -2,108 +2,36 @@ import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.13 import "../../../../shared" -import "../../../../imports" import "../components" - -Rectangle { - id: addChat +AddButton { + id: btnAdd width: 36 height: 36 - color: Theme.blue - radius: 50 - anchors.right: parent.right - anchors.rightMargin: 16 - anchors.top: parent.top - anchors.topMargin: 59 - Image { - id: addChatLbl - fillMode: Image.PreserveAspectFit - source: "../../../img/plusSign.svg" - width: 14 - height: 14 - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - - state: "default" - rotation: 0 - states: [ - State { - name: "default" - PropertyChanges { - target: addChatLbl - rotation: 0 - } - }, - State { - name: "rotated" - PropertyChanges { - target: addChatLbl - 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 - } - } - ] + onClicked: { + let x = btnAdd.icon.x + btnAdd.icon.width / 2 - newChatMenu.width / 2 + newChatMenu.popup(x, btnAdd.icon.height + 10) } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - cursorShape: Qt.PointingHandCursor - onClicked: { - addChatLbl.state = "rotated" - let x = addChatLbl.x + addChatLbl.width / 2 - newChatMenu.width / 2 - newChatMenu.popup(x, addChatLbl.height + 10) + + PopupMenu { + id: newChatMenu + Action { + text: qsTr("Start new chat") + icon.source: "../../../img/new_chat.svg" + onTriggered: privateChatPopup.open() } - - PopupMenu { - id: newChatMenu - Action { - text: qsTr("Start new chat") - icon.source: "../../../img/new_chat.svg" - onTriggered: privateChatPopup.open() - } - Action { - text: qsTr("Start group chat") - icon.source: "../../../img/group_chat.svg" - onTriggered: { - onTriggered: groupChatPopup.open() - } - } - Action { - text: qsTr("Join public chat") - icon.source: "../../../img/public_chat.svg" - onTriggered: publicChatPopup.open() - } - onAboutToHide: { - addChatLbl.state = "default" - } + Action { + text: qsTr("Start group chat") + icon.source: "../../../img/group_chat.svg" + onTriggered: groupChatPopup.open() + } + Action { + text: qsTr("Join public chat") + icon.source: "../../../img/public_chat.svg" + onTriggered: publicChatPopup.open() + } + onAboutToHide: { + btnAdd.icon.state = "default" } } } - -/*##^## -Designer { - D{i:0;formeditorZoom:3} -} -##^##*/ diff --git a/ui/app/AppLayouts/Chat/components/Contact.qml b/ui/app/AppLayouts/Chat/components/Contact.qml index 8c8b69f8aa..8759d7cc5a 100644 --- a/ui/app/AppLayouts/Chat/components/Contact.qml +++ b/ui/app/AppLayouts/Chat/components/Contact.qml @@ -27,7 +27,7 @@ Rectangle { border.width: 0 radius: Theme.radius - RoundImage { + Identicon { id: accountImage anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter diff --git a/ui/app/AppLayouts/Chat/components/StickersPopup.qml b/ui/app/AppLayouts/Chat/components/StickersPopup.qml new file mode 100644 index 0000000000..520d36d70b --- /dev/null +++ b/ui/app/AppLayouts/Chat/components/StickersPopup.qml @@ -0,0 +1,192 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 +import "../../../../imports" +import "../../../../shared" +import "../ChatColumn/samples" + +Popup { + id: popup + property var stickerList: StickerData {} + property var stickerPackList: StickerPackData {} + modal: false + property int selectedPackId + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + background: Rectangle { + radius: 8 + border.color: Theme.grey + layer.enabled: true + layer.effect: DropShadow{ + verticalOffset: 3 + radius: 8 + samples: 15 + fast: true + cached: true + color: "#22000000" + } + } + contentItem: ColumnLayout { + parent: popup + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.leftMargin: 4 + Layout.rightMargin: 4 + Layout.topMargin: 4 + Layout.bottomMargin: 0 + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.preferredHeight: 400 - 4 + + Item { + id: stickerHistory + anchors.fill: parent + visible: true + + Image { + id: imgNoStickers + width: 56 + height: 56 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 134 + source: "../../../img/stickers_sad_icon.svg" + } + + Text { + id: lblNoStickers + width: parent.width + font.pixelSize: 15 + text: qsTr("You don't have any stickers yet") + horizontalAlignment: Text.AlignHCenter + anchors.top: imgNoStickers.bottom + anchors.topMargin: 8 + } + + StyledButton { + label: qsTr("Get Stickers") + anchors.top: lblNoStickers.bottom + anchors.topMargin: Theme.padding + anchors.horizontalCenter: parent.horizontalCenter + } + } + + GridView { + id: stickerGrid + visible: false + anchors.fill: parent + cellWidth: 88 + cellHeight: 88 + model: stickerList + focus: true + clip: true + delegate: Item { + width: stickerGrid.cellWidth + height: stickerGrid.cellHeight + Column { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 4 + Image { + width: 80 + height: 80 + fillMode: Image.PreserveAspectFit + source: "https://ipfs.infura.io/ipfs/" + url + MouseArea { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + onClicked: { + chatsModel.sendSticker(hash, popup.selectedPackId) + popup.close() + } + } + } + } + } + } + } + + Item { + id: footerContent + Layout.leftMargin: 8 + Layout.fillWidth: true + Layout.preferredHeight: 40 - 8 * 2 + Layout.topMargin: 8 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + + AddButton { + id: btnAddStickerPack + anchors.left: parent.left + anchors.top: parent.top + width: 24 + height: 24 + } + + RoundedIcon { + id: btnHistory + size: 24 + color: Theme.darkGrey + imgPath: "../../../img/history_icon.svg" + anchors.left: btnAddStickerPack.right + anchors.leftMargin: Theme.padding + onClicked: { + packIndicator.updatePosition(-1) + stickerGrid.visible = false; + stickerHistory.visible = true; + } + } + + RowLayout { + spacing: Theme.padding + anchors.top: parent.top + anchors.left: btnHistory.right + anchors.leftMargin: Theme.padding + + Repeater { + id: stickerPackListView + model: stickerPackList + + delegate: RoundedImage { + Layout.preferredHeight: height + Layout.preferredWidth: width + width: 24 + height: 24 + source: "https://ipfs.infura.io/ipfs/" + thumbnail + onClicked: { + chatsModel.setActiveStickerPackById(id) + popup.selectedPackId = id + packIndicator.updatePosition(index) + stickerGrid.visible = true; + stickerHistory.visible = false; + } + } + } + } + Rectangle { + id: packIndicator + border.color: Theme.blue + border.width: 1 + height: 2 + width: 16 + x: 44 + y: footerContent.height + 8 - height + + function updatePosition(index) { + const startX = 44 + const skipX = 40 + const idx = index + 1 + packIndicator.x = startX + skipX * idx; + } + } + } + } +} +/*##^## +Designer { + D{i:0;formeditorColor:"#ffffff";height:440;width:360} +} +##^##*/ diff --git a/ui/app/AppLayouts/Chat/components/qmldir b/ui/app/AppLayouts/Chat/components/qmldir index 7c30c0cf06..18209098dd 100644 --- a/ui/app/AppLayouts/Chat/components/qmldir +++ b/ui/app/AppLayouts/Chat/components/qmldir @@ -5,4 +5,5 @@ GroupInfoPopup 1.0 GroupInfoPopup.qml ProfilePropup 1.0 ProfilePopup.qml ChannelIcon 1.0 ChannelIcon.qml RenameGroupPopup 1.0 RenameGroupPopup.qml -GroupChatPopup 1.0 GroupChatPopup.qml \ No newline at end of file +GroupChatPopup 1.0 GroupChatPopup.qml +StickersPopup 1.0 StickersPopup.qml diff --git a/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml b/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml index f033bcfc2e..c3c4f25f5e 100644 --- a/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml +++ b/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml @@ -19,7 +19,7 @@ Rectangle { border.width: 0 radius: Theme.radius - RoundImage { + Identicon { id: accountImage anchors.left: parent.left anchors.leftMargin: Theme.padding diff --git a/ui/app/AppLayouts/Wallet/components/AddAccount.qml b/ui/app/AppLayouts/Wallet/components/AddAccount.qml index cec558aeaf..2f19c85c80 100644 --- a/ui/app/AppLayouts/Wallet/components/AddAccount.qml +++ b/ui/app/AppLayouts/Wallet/components/AddAccount.qml @@ -3,124 +3,59 @@ import QtQuick.Controls 2.13 import "../../../../shared" import "../../../../imports" -Rectangle { - id: addAccount - width: 36 - height: 36 - color: Theme.blue - radius: 50 - anchors.right: parent.right - anchors.rightMargin: 16 - anchors.top: parent.top - anchors.topMargin: 59 - - Image { - id: addAccountLbl - fillMode: Image.PreserveAspectFit - source: "../../../img/plusSign.svg" - width: 14 - height: 14 - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - - state: "default" - rotation: 0 - states: [ - State { - name: "default" - PropertyChanges { - target: addAccountLbl - rotation: 0 - } - }, - State { - name: "rotated" - PropertyChanges { - target: addAccountLbl - 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 - } - } - ] +AddButton { + id: btnAdd + onClicked: { + let x = btnAdd.icon.x + btnAdd.icon.width / 2 - newAccountMenu.width / 2 + newAccountMenu.popup(x, btnAdd.icon.height + 10) } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - cursorShape: Qt.PointingHandCursor - onClicked: { - addAccountLbl.state = "rotated" - let x = addAccountLbl.x + addAccountLbl.width / 2 - newAccountMenu.width / 2 - newAccountMenu.popup(x, addAccountLbl.height + 10) - } + GenerateAccountModal { + id: generateAccountModal + } + AddAccountWithSeed { + id: addAccountWithSeedModal + } + AddAccountWithPrivateKey { + id: addAccountWithPrivateKeydModal + } + AddWatchOnlyAccount { + id: addWatchOnlyAccountModal + } - GenerateAccountModal { - id: generateAccountModal + PopupMenu { + id: newAccountMenu + width: 280 + Action { + text: qsTr("Generate an account") + icon.source: "../../../img/generate_account.svg" + onTriggered: { + generateAccountModal.open() + } } - AddAccountWithSeed { - id: addAccountWithSeedModal + Action { + text: qsTr("Add a watch-only address") + icon.source: "../../../img/add_watch_only.svg" + onTriggered: { + addWatchOnlyAccountModal.open() + } } - AddAccountWithPrivateKey { - id: addAccountWithPrivateKeydModal + Action { + text: qsTr("Enter a seed phrase") + icon.source: "../../../img/enter_seed_phrase.svg" + onTriggered: { + addAccountWithSeedModal.open() + } } - AddWatchOnlyAccount { - id: addWatchOnlyAccountModal + Action { + text: qsTr("Enter a private key") + icon.source: "../../../img/enter_private_key.svg" + onTriggered: { + addAccountWithPrivateKeydModal.open() + } } - - PopupMenu { - id: newAccountMenu - width: 280 - Action { - text: qsTr("Generate an account") - icon.source: "../../../img/generate_account.svg" - onTriggered: { - generateAccountModal.open() - } - } - Action { - text: qsTr("Add a watch-only address") - icon.source: "../../../img/add_watch_only.svg" - onTriggered: { - addWatchOnlyAccountModal.open() - } - } - Action { - text: qsTr("Enter a seed phrase") - icon.source: "../../../img/enter_seed_phrase.svg" - onTriggered: { - addAccountWithSeedModal.open() - } - } - Action { - text: qsTr("Enter a private key") - icon.source: "../../../img/enter_private_key.svg" - onTriggered: { - addAccountWithPrivateKeydModal.open() - } - } - onAboutToHide: { - addAccountLbl.state = "default" - } + onAboutToHide: { + btnAdd.icon.state = "default" } } } diff --git a/ui/app/img/history_icon.svg b/ui/app/img/history_icon.svg new file mode 100644 index 0000000000..b40d6fdc84 --- /dev/null +++ b/ui/app/img/history_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/img/stickers_icon.svg b/ui/app/img/stickers_icon.svg new file mode 100644 index 0000000000..7623d989b3 --- /dev/null +++ b/ui/app/img/stickers_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/app/img/stickers_icon_open.svg b/ui/app/img/stickers_icon_open.svg new file mode 100644 index 0000000000..7d6686ec65 --- /dev/null +++ b/ui/app/img/stickers_icon_open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/app/img/stickers_sad_icon.svg b/ui/app/img/stickers_sad_icon.svg new file mode 100644 index 0000000000..922b0808e9 --- /dev/null +++ b/ui/app/img/stickers_sad_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/nim-status-client.pro b/ui/nim-status-client.pro index d032a5e5f9..fd1bde6cf2 100644 --- a/ui/nim-status-client.pro +++ b/ui/nim-status-client.pro @@ -72,8 +72,11 @@ DISTFILES += \ Theme.qml \ app/AppLayouts/Browser/BrowserLayout.qml \ app/AppLayouts/Chat/ChatColumn.qml \ - app/AppLayouts/Chat/ChatColumn/MessagesData.qml \ + app/AppLayouts/Chat/ChatColumn/samples/MessagesData.qml \ + app/AppLayouts/Chat/ChatColumn/samples/StickerData.qml \ + app/AppLayouts/Chat/ChatColumn/samples/StickerPackData.qml \ app/AppLayouts/Chat/ChatColumn/ChatInput.qml \ + app/AppLayouts/Chat/ChatColumn/ChatButtons.qml \ app/AppLayouts/Chat/ChatColumn/ChatMessages.qml \ app/AppLayouts/Chat/ChatColumn/EmptyChat.qml \ app/AppLayouts/Chat/ChatColumn/Message.qml \ @@ -81,7 +84,6 @@ DISTFILES += \ app/AppLayouts/Chat/ChatColumn/qmldir \ app/AppLayouts/Chat/ChatLayout.qml \ app/AppLayouts/Chat/ContactsColumn.qml \ - app/AppLayouts/Chat/ContactsColumn/AddChat.qml \ app/AppLayouts/Chat/ContactsColumn/Channel.qml \ app/AppLayouts/Chat/ContactsColumn/ChannelList.qml \ app/AppLayouts/Chat/ContactsColumn/EmptyView.qml \ @@ -91,6 +93,7 @@ DISTFILES += \ app/AppLayouts/Chat/components/PrivateChatPopup.qml \ app/AppLayouts/Chat/components/RenameGroupPopup.qml \ app/AppLayouts/Chat/components/SuggestedChannel.qml \ + app/AppLayouts/Chat/components/StickersPopup.qml \ app/AppLayouts/Chat/components/qmldir \ app/AppLayouts/Chat/qmldir \ app/AppLayouts/Node/NodeLayout.qml \ @@ -151,6 +154,7 @@ DISTFILES += \ app/img/compassActive.svg \ app/img/group_chat.svg \ app/img/hash.svg \ + app/img/history_icon.svg \ app/img/message.svg \ app/img/messageActive.svg \ app/img/new_chat.svg \ @@ -159,6 +163,9 @@ DISTFILES += \ app/img/public_chat.svg \ app/img/search.svg \ app/img/wallet.svg \ + app/img/stickers_icon.svg \ + app/img/stickers_icon_open.svg \ + app/img/stickers_sad_icon.svg \ app/img/walletActive.svg \ app/qmldir \ imports/Utils.qml \ @@ -196,10 +203,12 @@ DISTFILES += \ onboarding/img/wallet@2x.jpg \ onboarding/img/wallet@3x.jpg \ onboarding/qmldir \ + shared/AddButton.qml \ shared/Input.qml \ shared/ModalPopup.qml \ shared/PopupMenu.qml \ - shared/RoundImage.qml \ + shared/Identicon.qml \ + shared/RoundedImage.qml \ shared/SearchBox.qml \ shared/Select.qml \ shared/Separator.qml \ diff --git a/ui/onboarding/Login.qml b/ui/onboarding/Login.qml index 631239671d..8bf338d7dc 100644 --- a/ui/onboarding/Login.qml +++ b/ui/onboarding/Login.qml @@ -25,7 +25,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - RoundImage { + Identicon { id: userImage width: 40 height: 40 diff --git a/ui/onboarding/Login/AddressView.qml b/ui/onboarding/Login/AddressView.qml index 774673fb0f..47dc04d36d 100644 --- a/ui/onboarding/Login/AddressView.qml +++ b/ui/onboarding/Login/AddressView.qml @@ -22,7 +22,7 @@ Rectangle { color: selected || isHovered ? Theme.grey : Theme.transparent radius: Theme.radius - RoundImage { + Identicon { id: accountImage anchors.left: parent.left anchors.leftMargin: Theme.padding diff --git a/ui/shared/AddButton.qml b/ui/shared/AddButton.qml new file mode 100644 index 0000000000..e5ce0efa8e --- /dev/null +++ b/ui/shared/AddButton.qml @@ -0,0 +1,80 @@ +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 + + id: btnAddContainer + width: 36 + height: 36 + color: Theme.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 + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: { + imgIcon.state = "rotated" + btnAddContainer.clicked() + } + } +} \ No newline at end of file diff --git a/ui/shared/RoundImage.qml b/ui/shared/Identicon.qml similarity index 100% rename from ui/shared/RoundImage.qml rename to ui/shared/Identicon.qml diff --git a/ui/shared/RoundedIcon.qml b/ui/shared/RoundedIcon.qml index 305bc584ff..ade1955828 100644 --- a/ui/shared/RoundedIcon.qml +++ b/ui/shared/RoundedIcon.qml @@ -2,14 +2,16 @@ import QtQuick 2.13 import "../imports" Rectangle { + id: root property int size: 36 property color bg: Theme.blue property url imgPath: "" + signal clicked width: size height: size color: bg - radius: 50 + radius: size / 2 Image { id: roundedIconImage @@ -20,6 +22,16 @@ Rectangle { fillMode: Image.PreserveAspectFit source: imgPath } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: { + root.clicked() + } + } } /*##^## diff --git a/ui/shared/RoundedImage.qml b/ui/shared/RoundedImage.qml new file mode 100644 index 0000000000..9b1b050647 --- /dev/null +++ b/ui/shared/RoundedImage.qml @@ -0,0 +1,38 @@ +import QtQuick 2.12 +import QtGraphicalEffects 1.0 + +Rectangle { + id: root; + signal clicked + property alias source: imgStickerPackThumb.source + + radius: width / 2 + + width: 24 + height: 24 + + // apply rounded corners mask + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + x: root.x; y: root.y + width: root.width + height: root.height + radius: root.radius + } + } + + Image { + id: imgStickerPackThumb + opacity: 1 + smooth: false + anchors.fill: parent + source: "https://ipfs.infura.io/ipfs/" + thumbnail + + MouseArea { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + onClicked: root.clicked() + } + } +} \ No newline at end of file diff --git a/ui/shared/qmldir b/ui/shared/qmldir index b8acdc5dc4..dc92706b01 100644 --- a/ui/shared/qmldir +++ b/ui/shared/qmldir @@ -12,4 +12,5 @@ StyledTextArea 1.0 StyledTextArea.qml StyledText 1.0 StyledText.qml StyledTextField 1.0 StyledTextField.qml StyledTextEdit 1.0 StyledTextEdit.qml -RoundImage 1.0 RoundImage.qml +Identicon 1.0 Identicon.qml +RoundedImage 1.0 RoundedImage.qml