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) INFURA_TOKEN ?= $(DEFAULT_TOKEN)
NIM_PARAMS += -d:INFURA_TOKEN:"$(INFURA_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 # 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)]" 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/chat/[chat, message]
import ../../status/profile/profile import ../../status/profile/profile
import web3/[conversions, ethtypes] 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 ../utils/image_utils
import ../../status/tasks/[qt, task_runner_impl] import ../../status/tasks/[qt, task_runner_impl]
import ../../status/tasks/marathon/mailserver/worker import ../../status/tasks/marathon/mailserver/worker
@ -81,6 +81,7 @@ QtObject:
callResult: string callResult: string
reactions*: ReactionView reactions*: ReactionView
stickers*: StickersView stickers*: StickersView
gif*: GifView
groups*: GroupsView groups*: GroupsView
transactions*: TransactionsView transactions*: TransactionsView
communities*: CommunitiesView communities*: CommunitiesView
@ -98,6 +99,7 @@ QtObject:
self.activityNotificationList.delete self.activityNotificationList.delete
self.reactions.delete self.reactions.delete
self.stickers.delete self.stickers.delete
self.gif.delete
self.groups.delete self.groups.delete
self.transactions.delete self.transactions.delete
self.communities.delete self.communities.delete
@ -121,6 +123,7 @@ QtObject:
result.channelView.activeChannel result.channelView.activeChannel
) )
result.stickers = newStickersView(status, result.channelView.activeChannel) result.stickers = newStickersView(status, result.channelView.activeChannel)
result.gif = newGifView()
result.groups = newGroupsView(status,result.channelView.activeChannel) result.groups = newGroupsView(status,result.channelView.activeChannel)
result.transactions = newTransactionsView(status) result.transactions = newTransactionsView(status)
@ -356,6 +359,12 @@ QtObject:
QtProperty[QVariant] stickers: QtProperty[QVariant] stickers:
read = getStickers read = getStickers
proc getGif*(self: ChatsView): QVariant {.slot.} =
newQVariant(self.gif)
QtProperty[QVariant] gif:
read = getGif
proc getGroups*(self: ChatsView): QVariant {.slot.} = proc getGroups*(self: ChatsView): QVariant {.slot.} =
newQVariant(self.groups) 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 { // StatusSettingsLineButton {
// //% "Node Management" // //% "Node Management"
// text: qsTrId("node-management") // text: qsTrId("node-management")

View File

@ -348,6 +348,7 @@ StatusAppLayout {
property bool isBrowserEnabled: false property bool isBrowserEnabled: false
property bool isActivityCenterEnabled: false property bool isActivityCenterEnabled: false
property bool showOnlineUsers: false property bool showOnlineUsers: false
property bool isGifWidgetEnabled: false
property bool displayChatImages: false property bool displayChatImages: false
property bool useCompactMode: true property bool useCompactMode: true
property bool timelineEnabled: true property bool timelineEnabled: true

View File

@ -79,6 +79,28 @@ Rectangle {
messageInputField.insert(start, text.replace(/\n/g, "<br/>")); 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) { property var interpretMessage: function (msg) {
if (msg.startsWith("/shrug")) { if (msg.startsWith("/shrug")) {
return msg.replace("/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 { StatusEmojiPopup {
id: emojiPopup id: emojiPopup
width: 360 width: 360
@ -1042,15 +1081,22 @@ Rectangle {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
icon.name: "emojiBtn" icon.name: "emojiBtn"
type: "secondary" type: "secondary"
onClicked: { onClicked: togglePopup(emojiPopup, emojiBtn)
stickersPopup.close()
if (emojiPopup.opened) {
emojiPopup.close()
highlighted = false
} else {
emojiPopup.open()
highlighted = true
} }
StatusIconButton {
id: gifBtn
visible: appSettings.isGifWidgetEnabled
anchors.right: emojiBtn.left
anchors.rightMargin: 2
anchors.bottom: parent.bottom
icon.name: "wallet"
type: "secondary"
onClicked: {
if (!gifPopup.opened) {
chatsModel.gif.load()
}
togglePopup(gifPopup, gifBtn)
} }
} }
@ -1063,16 +1109,7 @@ Rectangle {
visible: !isEdit && profileModel.network.current === Constants.networkMainnet && emojiBtn.visible visible: !isEdit && profileModel.network.current === Constants.networkMainnet && emojiBtn.visible
width: visible ? 32 : 0 width: visible ? 32 : 0
type: "secondary" type: "secondary"
onClicked: { onClicked: togglePopup(stickersPopup, stickersBtn)
emojiPopup.close()
if (stickersPopup.opened) {
stickersPopup.close()
highlighted = false
} else {
stickersPopup.open()
highlighted = true
}
}
} }
} }
} }

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 StatusEmojiCategoryButton 1.0 StatusEmojiCategoryButton.qml
StatusEmojiPopup 1.0 StatusEmojiPopup.qml StatusEmojiPopup 1.0 StatusEmojiPopup.qml
StatusEmojiSection 1.0 StatusEmojiSection.qml StatusEmojiSection 1.0 StatusEmojiSection.qml
StatusGifPopup 1.0 StatusGifPopup.qml
StatusIconButton 1.0 StatusIconButton.qml StatusIconButton 1.0 StatusIconButton.qml
StatusImageIdenticon 1.0 StatusImageIdenticon.qml StatusImageIdenticon 1.0 StatusImageIdenticon.qml
StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml