From 58506fbd9737db9af2094e8974ee940245a745c9 Mon Sep 17 00:00:00 2001 From: Anthony Laibe Date: Wed, 21 Jul 2021 10:26:31 +0200 Subject: [PATCH] feat(@desktop/chat): Display gif popup next to emoji --- Makefile | 4 + src/app/chat/view.nim | 11 +- src/app/chat/views/gif.nim | 44 ++++++ src/app/chat/views/gif_list.nim | 50 +++++++ src/status/gif.nim | 60 ++++++++ .../Profile/Sections/AdvancedContainer.qml | 9 ++ ui/app/AppMain.qml | 1 + ui/shared/status/StatusChatInput.qml | 71 ++++++--- ui/shared/status/StatusGifPopup.qml | 136 ++++++++++++++++++ ui/shared/status/qmldir | 1 + 10 files changed, 369 insertions(+), 18 deletions(-) create mode 100644 src/app/chat/views/gif.nim create mode 100644 src/app/chat/views/gif_list.nim create mode 100644 src/status/gif.nim create mode 100644 ui/shared/status/StatusGifPopup.qml diff --git a/Makefile b/Makefile index 38e2a7f003..07b754e8fd 100644 --- a/Makefile +++ b/Makefile @@ -221,6 +221,10 @@ DEFAULT_TOKEN := 220a1abb4b6943a093c35d0ce4fb0732 INFURA_TOKEN ?= $(DEFAULT_TOKEN) NIM_PARAMS += -d:INFURA_TOKEN:"$(INFURA_TOKEN)" +DEFAULT_TENOR_API_KEY := DU7DWZ27STB2 +TENOR_API_KEY ?= $(DEFAULT_TENOR_API_KEY) +NIM_PARAMS += -d:TENOR_API_KEY:"$(TENOR_API_KEY)" + # generate a status-desktop.log file with chronicles output. This file will be truncated each time the app starts NIM_PARAMS += -d:chronicles_sinks="textlines[stdout],textlines[nocolors,file(status-desktop.log,truncate)]" diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index 1cdc1b8cb8..60bd6fbf4b 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -10,7 +10,7 @@ import ../../status/ens as status_ens import ../../status/chat/[chat, message] import ../../status/profile/profile import web3/[conversions, ethtypes] -import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item, format_input, ens, activity_notification_list, channel, messages, message_item] +import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item, format_input, ens, activity_notification_list, channel, messages, message_item, gif] import ../utils/image_utils import ../../status/tasks/[qt, task_runner_impl] import ../../status/tasks/marathon/mailserver/worker @@ -81,6 +81,7 @@ QtObject: callResult: string reactions*: ReactionView stickers*: StickersView + gif*: GifView groups*: GroupsView transactions*: TransactionsView communities*: CommunitiesView @@ -98,6 +99,7 @@ QtObject: self.activityNotificationList.delete self.reactions.delete self.stickers.delete + self.gif.delete self.groups.delete self.transactions.delete self.communities.delete @@ -121,6 +123,7 @@ QtObject: result.channelView.activeChannel ) result.stickers = newStickersView(status, result.channelView.activeChannel) + result.gif = newGifView() result.groups = newGroupsView(status,result.channelView.activeChannel) result.transactions = newTransactionsView(status) @@ -356,6 +359,12 @@ QtObject: QtProperty[QVariant] stickers: read = getStickers + proc getGif*(self: ChatsView): QVariant {.slot.} = + newQVariant(self.gif) + + QtProperty[QVariant] gif: + read = getGif + proc getGroups*(self: ChatsView): QVariant {.slot.} = newQVariant(self.groups) diff --git a/src/app/chat/views/gif.nim b/src/app/chat/views/gif.nim new file mode 100644 index 0000000000..c8666705b2 --- /dev/null +++ b/src/app/chat/views/gif.nim @@ -0,0 +1,44 @@ +import # vendor libs + NimQml + +import gif_list +import ../../../status/gif + + +QtObject: + type GifView* = ref object of QObject + items*: GifList + client: GifClient + + proc setup(self: GifView) = + self.QObject.setup + + proc delete*(self: GifView) = + self.QObject.delete + + proc newGifView*(): GifView = + new(result, delete) + result = GifView() + result.client = newGifClient() + result.items = newGifList() + result.setup() + + proc getItemsList*(self: GifView): QVariant {.slot.} = + result = newQVariant(self.items) + + proc itemsLoaded*(self: GifView) {.signal.} + + QtProperty[QVariant] items: + read = getItemsList + notify = itemsLoaded + + proc load*(self: GifView) {.slot.} = + let data = self.client.getTrendings() + self.items.setNewData(data) + self.itemsLoaded() + + proc search*(self: GifView, query: string) {.slot.} = + let data = self.client.search(query) + self.items.setNewData(data) + self.itemsLoaded() + \ No newline at end of file diff --git a/src/app/chat/views/gif_list.nim b/src/app/chat/views/gif_list.nim new file mode 100644 index 0000000000..c248bda362 --- /dev/null +++ b/src/app/chat/views/gif_list.nim @@ -0,0 +1,50 @@ +import NimQml, Tables, sequtils + +import ../../../status/gif + +type + GifRoles {.pure.} = enum + Url = UserRole + 1 + Id = UserRole + 2 + Title = UserRole + 3 + +QtObject: + type + GifList* = ref object of QAbstractListModel + gifs*: seq[GifItem] + + proc setup(self: GifList) = self.QAbstractListModel.setup + + proc delete(self: GifList) = self.QAbstractListModel.delete + + proc newGifList*(): GifList = + new(result, delete) + result.gifs = @[] + result.setup() + + proc setNewData*(self: GifList, gifs: seq[GifItem]) = + self.beginResetModel() + self.gifs = gifs + self.endResetModel() + + method rowCount(self: GifList, index: QModelIndex = nil): int = self.gifs.len + + method data(self: GifList, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + + if index.row < 0 or index.row >= self.gifs.len: + return + + let gif = self.gifs[index.row] + case role.GifRoles: + of GifRoles.Url: result = newQVariant(gif.url) + of GifRoles.Id: result = newQVariant(gif.id) + of GifRoles.Title: result = newQVariant(gif.title) + + method roleNames(self: GifList): Table[int, string] = + { + GifRoles.Url.int:"url", + GifRoles.Id.int:"id", + GifRoles.Title.int:"title" + }.toTable \ No newline at end of file diff --git a/src/status/gif.nim b/src/status/gif.nim new file mode 100644 index 0000000000..6619170c77 --- /dev/null +++ b/src/status/gif.nim @@ -0,0 +1,60 @@ +import httpclient +import json +import strformat +import os + +# set via `nim c` param `-d:TENOR_API_KEY:[api_key]`; should be set in CI/release builds +const TENOR_API_KEY {.strdefine.} = "" +let TENOR_API_KEY_ENV = $getEnv("TENOR_API_KEY") + +let TENOR_API_KEY_RESOLVED = + if TENOR_API_KEY_ENV != "": + TENOR_API_KEY_ENV + else: + TENOR_API_KEY + +const baseUrl = "https://g.tenor.com/v1/" +let defaultParams = fmt("&media_filter=basic&limit=10&key={TENOR_API_KEY_RESOLVED}") + +type + GifItem* = object + id*: int + title*: string + url*: string + +proc toGifItem(jsonMsg: JsonNode): GifItem = + return GifItem( + id: jsonMsg{"id"}.getInt, + title: jsonMsg{"title"}.getStr, + url: jsonMsg{"media"}[0]["gif"]["url"].getStr + ) + +proc `$`*(self: GifItem): string = + return fmt"GifItem(id:{self.id}, title:{self.title}, url:{self.url})" + +type + GifClient* = ref object + client: HttpClient + +proc newGifClient*(): GifClient = + result = GifClient() + result.client = newHttpClient() + +proc tenorQuery(self: GifClient, path: string): seq[GifItem] = + try: + let content = self.client.getContent(fmt("{baseUrl}{path}{defaultParams}")) + let doc = content.parseJson() + + var items: seq[GifItem] = @[] + for json in doc["results"]: + items.add(toGifItem(json)) + + return items + except: + return @[] + +proc search*(self: GifClient, query: string): seq[GifItem] = + return self.tenorQuery(fmt("search?q={query}")) + +proc getTrendings*(self: GifClient): seq[GifItem] = + return self.tenorQuery("trending?") \ No newline at end of file diff --git a/ui/app/AppLayouts/Profile/Sections/AdvancedContainer.qml b/ui/app/AppLayouts/Profile/Sections/AdvancedContainer.qml index 0b6599e335..ae6bf3c055 100644 --- a/ui/app/AppLayouts/Profile/Sections/AdvancedContainer.qml +++ b/ui/app/AppLayouts/Profile/Sections/AdvancedContainer.qml @@ -230,6 +230,15 @@ Item { } } + StatusSettingsLineButton { + text: qsTr("GIF Widget") + isSwitch: true + switchChecked: appSettings.isGifWidgetEnabled + onClicked: { + appSettings.isGifWidgetEnabled = !appSettings.isGifWidgetEnabled + } + } + // StatusSettingsLineButton { // //% "Node Management" // text: qsTrId("node-management") diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index ad16d39712..cf572c530b 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -348,6 +348,7 @@ StatusAppLayout { property bool isBrowserEnabled: false property bool isActivityCenterEnabled: false property bool showOnlineUsers: false + property bool isGifWidgetEnabled: false property bool displayChatImages: false property bool useCompactMode: true property bool timelineEnabled: true diff --git a/ui/shared/status/StatusChatInput.qml b/ui/shared/status/StatusChatInput.qml index e3f78705d8..944c546d85 100644 --- a/ui/shared/status/StatusChatInput.qml +++ b/ui/shared/status/StatusChatInput.qml @@ -79,6 +79,28 @@ Rectangle { messageInputField.insert(start, text.replace(/\n/g, "
")); } + function togglePopup(popup, btn) { + if (popup !== stickersPopup) { + stickersPopup.close() + } + + if (popup !== gifPopup) { + gifPopup.close() + } + + if (popup !== emojiPopup) { + emojiPopup.close() + } + + if (popup.opened) { + popup.close() + btn.highlighted = false + } else { + popup.open() + btn.highlighted = true + } + } + property var interpretMessage: function (msg) { if (msg.startsWith("/shrug")) { return msg.replace("/shrug", "") + " ¯\\\\\\_(ツ)\\_/¯" @@ -614,6 +636,23 @@ Rectangle { } } + StatusGifPopup { + id: gifPopup + width: 360 + height: 440 + x: parent.width - width - Style.current.halfPadding + y: -height + gifSelected: function (event, url) { + messageInputField.text = url + control.sendMessage(event) + gifBtn.highlighted = false + messageInputField.forceActiveFocus(); + } + onClosed: { + gifBtn.highlighted = false + } + } + StatusEmojiPopup { id: emojiPopup width: 360 @@ -1042,15 +1081,22 @@ Rectangle { anchors.bottom: parent.bottom icon.name: "emojiBtn" type: "secondary" + onClicked: togglePopup(emojiPopup, emojiBtn) + } + + StatusIconButton { + id: gifBtn + visible: appSettings.isGifWidgetEnabled + anchors.right: emojiBtn.left + anchors.rightMargin: 2 + anchors.bottom: parent.bottom + icon.name: "wallet" + type: "secondary" onClicked: { - stickersPopup.close() - if (emojiPopup.opened) { - emojiPopup.close() - highlighted = false - } else { - emojiPopup.open() - highlighted = true + if (!gifPopup.opened) { + chatsModel.gif.load() } + togglePopup(gifPopup, gifBtn) } } @@ -1063,16 +1109,7 @@ Rectangle { visible: !isEdit && profileModel.network.current === Constants.networkMainnet && emojiBtn.visible width: visible ? 32 : 0 type: "secondary" - onClicked: { - emojiPopup.close() - if (stickersPopup.opened) { - stickersPopup.close() - highlighted = false - } else { - stickersPopup.open() - highlighted = true - } - } + onClicked: togglePopup(stickersPopup, stickersBtn) } } } diff --git a/ui/shared/status/StatusGifPopup.qml b/ui/shared/status/StatusGifPopup.qml new file mode 100644 index 0000000000..3bb8d0edf5 --- /dev/null +++ b/ui/shared/status/StatusGifPopup.qml @@ -0,0 +1,136 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 +import StatusQ.Components 0.1 +import "../../imports" +import "../../shared" + +Popup { + id: popup + property var loading: true + property var gifSelected: function () {} + property var searchGif: Backpressure.debounce(searchBox, 500, function (query) { + loading = true + chatsModel.gif.search(query) + }); + property alias searchString: searchBox.text + modal: false + width: 360 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + + background: Rectangle { + radius: Style.current.radius + color: Style.current.background + border.color: Style.current.border + layer.enabled: true + layer.effect: DropShadow{ + verticalOffset: 3 + radius: 8 + samples: 15 + fast: true + cached: true + color: "#22000000" + } + } + + onOpened: { + searchBox.text = "" + searchBox.forceActiveFocus(Qt.MouseFocusReason) + } + + Connections { + target: chatsModel.gif + onItemsLoaded: { + loading = false + } + } + + contentItem: ColumnLayout { + anchors.fill: parent + spacing: 0 + + Item { + property int headerMargin: 8 + + id: gifHeader + Layout.fillWidth: true + height: searchBox.height + gifHeader.headerMargin + + SearchBox { + id: searchBox + anchors.right: parent.right + anchors.rightMargin: gifHeader.headerMargin + anchors.top: parent.top + anchors.topMargin: gifHeader.headerMargin + anchors.left: parent.left + anchors.leftMargin: gifHeader.headerMargin + Keys.onReleased: { + Qt.callLater(searchGif, searchBox.text); + } + } + } + + Loader { + Layout.fillWidth: true + Layout.rightMargin: Style.current.smallPadding / 2 + Layout.topMargin: Style.current.smallPadding + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.preferredHeight: 400 - gifHeader.height + sourceComponent: loading ? gifLoading : gifItems + } + } + + Component { + id: gifLoading + StatusLoadingIndicator {} + } + + Component { + id: gifItems + ScrollView { + id: scrollView + property ScrollBar vScrollBar: ScrollBar.vertical + clip: true + topPadding: Style.current.smallPadding + leftPadding: Style.current.smallPadding + rightPadding: Style.current.smallPadding + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + Column { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + spacing: 5 + + Repeater { + id: gifSectionsRepeater + model: chatsModel.gif.items + + delegate: Rectangle { + width: animation.width + height: animation.height + + AnimatedImage { + id: animation + source: model.url + } + + MouseArea { + anchors.fill: parent + onClicked: function (event) { + gifSelected(event, model.url) + } + } + } + } + } + } + } +} +/*##^## +Designer { + D{i:0;formeditorColor:"#ffffff";height:440;width:360} +} +##^##*/ diff --git a/ui/shared/status/qmldir b/ui/shared/status/qmldir index eebaa7735b..f628918427 100644 --- a/ui/shared/status/qmldir +++ b/ui/shared/status/qmldir @@ -5,6 +5,7 @@ StatusChatInput 1.0 StatusChatInput.qml StatusEmojiCategoryButton 1.0 StatusEmojiCategoryButton.qml StatusEmojiPopup 1.0 StatusEmojiPopup.qml StatusEmojiSection 1.0 StatusEmojiSection.qml +StatusGifPopup 1.0 StatusGifPopup.qml StatusIconButton 1.0 StatusIconButton.qml StatusImageIdenticon 1.0 StatusImageIdenticon.qml StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml