feat(@desktop/chat): Display gif popup next to emoji

This commit is contained in:
Anthony Laibe 2021-07-21 10:26:31 +02:00 committed by Iuri Matias
parent 2df8e938ae
commit 58506fbd97
10 changed files with 369 additions and 18 deletions

View File

@ -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)]"

View File

@ -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)

View File

@ -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()

View File

@ -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

60
src/status/gif.nim Normal file
View File

@ -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?")

View File

@ -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")

View File

@ -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

View File

@ -79,6 +79,28 @@ Rectangle {
messageInputField.insert(start, text.replace(/\n/g, "<br/>"));
}
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)
}
}
}

View File

@ -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}
}
##^##*/

View File

@ -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