diff --git a/src/app/chat/views/gif.nim b/src/app/chat/views/gif.nim index 16b315bd87..dda3b03e47 100644 --- a/src/app/chat/views/gif.nim +++ b/src/app/chat/views/gif.nim @@ -22,9 +22,9 @@ QtObject: new(result, delete) result = GifView() result.client = newGifClient() - result.columnA = newGifList() - result.columnB = newGifList() - result.columnC = newGifList() + result.columnA = newGifList(result.client) + result.columnB = newGifList(result.client) + result.columnC = newGifList(result.client) result.setup() proc dataLoaded*(self: GifView) {.signal.} @@ -69,15 +69,51 @@ QtObject: columnCData.add(item) columnCHeight += item.height + self.columnA.setNewData(columnAData) self.columnB.setNewData(columnBData) self.columnC.setNewData(columnCData) self.dataLoaded() - proc load*(self: GifView) {.slot.} = + proc findGifItem(self: GifView, id: string): GifItem = + for item in self.columnA.gifs: + if item.id == id: + return item + + for item in self.columnB.gifs: + if item.id == id: + return item + + for item in self.columnC.gifs: + if item.id == id: + return item + + raise newException(ValueError, "Invalid id " & $id) + + proc getTrendings*(self: GifView) {.slot.} = let data = self.client.getTrendings() self.updateColumns(data) + proc getFavorites*(self: GifView) {.slot.} = + let data = self.client.getFavorites() + self.updateColumns(data) + + proc getRecents*(self: GifView) {.slot.} = + let data = self.client.getRecents() + self.updateColumns(data) + proc search*(self: GifView, query: string) {.slot.} = let data = self.client.search(query) self.updateColumns(data) + + proc toggleFavorite*(self: GifView, id: string, reload: bool = false) {.slot.} = + let gifItem = self.findGifItem(id) + self.client.toggleFavorite(gifItem) + + if reload: + self.getFavorites() + + proc addToRecents*(self: GifView, id: string) {.slot.} = + let gifItem = self.findGifItem(id) + self.client.addToRecents(gifItem) + \ No newline at end of file diff --git a/src/app/chat/views/gif_list.nim b/src/app/chat/views/gif_list.nim index 67d1af1377..acf3ea9241 100644 --- a/src/app/chat/views/gif_list.nim +++ b/src/app/chat/views/gif_list.nim @@ -8,19 +8,22 @@ type Id = UserRole + 2 Title = UserRole + 3 TinyUrl = UserRole + 4 + IsFavorite = UserRole + 5 QtObject: type GifList* = ref object of QAbstractListModel gifs*: seq[GifItem] + client: GifClient proc setup(self: GifList) = self.QAbstractListModel.setup proc delete(self: GifList) = self.QAbstractListModel.delete - proc newGifList*(): GifList = + proc newGifList*(client: GifClient): GifList = new(result, delete) result.gifs = @[] + result.client = client result.setup() proc setNewData*(self: GifList, gifs: seq[GifItem]) = @@ -43,11 +46,13 @@ QtObject: of GifRoles.Id: result = newQVariant(gif.id) of GifRoles.Title: result = newQVariant(gif.title) of GifRoles.TinyUrl: result = newQVariant(gif.tinyUrl) + of GifRoles.IsFavorite: result = newQVariant(self.client.isFavorite(gif)) method roleNames(self: GifList): Table[int, string] = { GifRoles.Url.int:"url", GifRoles.Id.int:"id", GifRoles.Title.int:"title", - GifRoles.TinyUrl.int:"tinyUrl" + GifRoles.TinyUrl.int:"tinyUrl", + GifRoles.IsFavorite.int:"isFavorite" }.toTable \ No newline at end of file diff --git a/src/status/gif.nim b/src/status/gif.nim index 2bdeb03b76..794f6f2a0a 100644 --- a/src/status/gif.nim +++ b/src/status/gif.nim @@ -2,7 +2,12 @@ import httpclient import json import strformat import os +import sequtils +from libstatus/gif import getRecentGifs, getFavoriteGifs, setFavoriteGifs, setRecentGifs + + +const MAX_RECENT = 50 # 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") @@ -18,31 +23,55 @@ let defaultParams = fmt("&media_filter=minimal&limit=50&key={TENOR_API_KEY_RESOL type GifItem* = object - id*: int + id*: string title*: string url*: string tinyUrl*: string height*: int -proc toGifItem(jsonMsg: JsonNode): GifItem = +proc tenorToGifItem(jsonMsg: JsonNode): GifItem = return GifItem( - id: jsonMsg{"id"}.getInt, + id: jsonMsg{"id"}.getStr, title: jsonMsg{"title"}.getStr, url: jsonMsg{"media"}[0]["gif"]["url"].getStr, tinyUrl: jsonMsg{"media"}[0]["tinygif"]["url"].getStr, height: jsonMsg{"media"}[0]["gif"]["dims"][1].getInt ) +proc settingToGifItem(jsonMsg: JsonNode): GifItem = + return GifItem( + id: jsonMsg{"id"}.getStr, + title: jsonMsg{"title"}.getStr, + url: jsonMsg{"url"}.getStr, + tinyUrl: jsonMsg{"tinyUrl"}.getStr, + height: jsonMsg{"height"}.getInt + ) + +proc toJsonNode*(self: GifItem): JsonNode = + result = %* { + "id": self.id, + "title": self.title, + "url": self.url, + "tinyUrl": self.tinyUrl, + "height": self.height + } + proc `$`*(self: GifItem): string = return fmt"GifItem(id:{self.id}, title:{self.title}, url:{self.url}, tinyUrl:{self.tinyUrl}, height:{self.height})" type GifClient* = ref object client: HttpClient + favorites: seq[GifItem] + recents: seq[GifItem] + favoritesLoaded: bool + recentsLoaded: bool proc newGifClient*(): GifClient = result = GifClient() result.client = newHttpClient() + result.favorites = @[] + result.recents = @[] proc tenorQuery(self: GifClient, path: string): seq[GifItem] = try: @@ -51,7 +80,7 @@ proc tenorQuery(self: GifClient, path: string): seq[GifItem] = var items: seq[GifItem] = @[] for json in doc["results"]: - items.add(toGifItem(json)) + items.add(tenorToGifItem(json)) return items except: @@ -62,4 +91,61 @@ 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 + return self.tenorQuery("trending?") + +proc getFavorites*(self: GifClient): seq[GifItem] = + if not self.favoritesLoaded: + self.favoritesLoaded = true + self.favorites = map(getFavoriteGifs(){"items"}.getElems(), settingToGifItem) + + return self.favorites + +proc getRecents*(self: GifClient): seq[GifItem] = + if not self.recentsLoaded: + self.recentsLoaded = true + self.recents = map(getRecentGifs(){"items"}.getElems(), settingToGifItem) + + return self.recents + +proc isFavorite*(self: GifClient, gifItem: GifItem): bool = + for favorite in self.getFavorites(): + if favorite.id == gifItem.id: + return true + + return false + +proc toggleFavorite*(self: GifClient, gifItem: GifItem) = + var newFavorites: seq[GifItem] = @[] + var found = false + + for favoriteGif in self.getFavorites(): + if favoriteGif.id == gifItem.id: + found = true + continue + + newFavorites.add(favoriteGif) + + if not found: + newFavorites.add(gifItem) + + self.favorites = newFavorites + setFavoriteGifs(%*{"items": map(newFavorites, toJsonNode)}) + +proc addToRecents*(self: GifClient, gifItem: GifItem) = + let recents = self.getRecents() + var newRecents: seq[GifItem] = @[gifItem] + var idx = 0 + + while idx < MAX_RECENT - 1: + if idx >= recents.len: + break + + if recents[idx].id == gifItem.id: + idx += 1 + continue + + newRecents.add(recents[idx]) + idx += 1 + + self.recents = newRecents + setRecentGifs(%*{"items": map(newRecents, toJsonNode)}) \ No newline at end of file diff --git a/src/status/libstatus/gif.nim b/src/status/libstatus/gif.nim new file mode 100644 index 0000000000..d75753ca52 --- /dev/null +++ b/src/status/libstatus/gif.nim @@ -0,0 +1,15 @@ +import json + +import ./settings, ../types + +proc getRecentGifs*(): JsonNode = + return settings.getSetting[JsonNode](Setting.Gifs_Recent, %*{}) + +proc getFavoriteGifs*(): JsonNode = + return settings.getSetting[JsonNode](Setting.Gifs_Favorite, %*{}) + +proc setFavoriteGifs*(items: JsonNode) = + discard settings.saveSetting(Setting.Gifs_Favorite, items) + +proc setRecentGifs*(items: JsonNode) = + discard settings.saveSetting(Setting.Gifs_Recent, items) \ No newline at end of file diff --git a/src/status/types.nim b/src/status/types.nim index c34a1dadde..252b2d928e 100644 --- a/src/status/types.nim +++ b/src/status/types.nim @@ -193,6 +193,8 @@ type DappsAddress = "dapps-address" Stickers_PacksInstalled = "stickers/packs-installed" Stickers_Recent = "stickers/recent-stickers" + Gifs_Recent = "gifs/recent-gifs" + Gifs_Favorite = "gifs/favorite-gifs" WalletRootAddress = "wallet-root-address" LatestDerivedPath = "latest-derived-path" PreferredUsername = "preferred-name" diff --git a/ui/app/img/gifCategories/favorite.svg b/ui/app/img/gifCategories/favorite.svg new file mode 100644 index 0000000000..f1f6d0c622 --- /dev/null +++ b/ui/app/img/gifCategories/favorite.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/img/gifCategories/recent.svg b/ui/app/img/gifCategories/recent.svg new file mode 100644 index 0000000000..46b6cce695 --- /dev/null +++ b/ui/app/img/gifCategories/recent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/app/img/gifCategories/trending.svg b/ui/app/img/gifCategories/trending.svg new file mode 100644 index 0000000000..4e47d9ea17 --- /dev/null +++ b/ui/app/img/gifCategories/trending.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/img/star-icon.svg b/ui/app/img/star-icon.svg new file mode 100644 index 0000000000..9c920edd39 --- /dev/null +++ b/ui/app/img/star-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/imports/Themes/DarkTheme.qml b/ui/imports/Themes/DarkTheme.qml index 883f9b9465..d0db17854b 100644 --- a/ui/imports/Themes/DarkTheme.qml +++ b/ui/imports/Themes/DarkTheme.qml @@ -32,6 +32,7 @@ Theme { property color fivePercentBlack: "#E5E5E5" property color tenPercentBlue: Qt.rgba(67, 96, 223, 0.1) property color orange: "#FFA500" + property color yellow: "#FFCA0F" property color background: "#2C2C2C" property color dropShadow: "#66000000" diff --git a/ui/imports/Themes/LightTheme.qml b/ui/imports/Themes/LightTheme.qml index 332afc3e75..87b4523ad2 100644 --- a/ui/imports/Themes/LightTheme.qml +++ b/ui/imports/Themes/LightTheme.qml @@ -32,6 +32,7 @@ Theme { property color fivePercentBlack: "#E5E5E5" property color tenPercentBlue: Qt.rgba(67, 96, 223, 0.1) property color orange: "#FFA500" + property color yellow: "#FFCA0F" property color background: white property color dropShadow: "#22000000" diff --git a/ui/shared/status/StatusEmojiCategoryButton.qml b/ui/shared/status/StatusCategoryButton.qml similarity index 100% rename from ui/shared/status/StatusEmojiCategoryButton.qml rename to ui/shared/status/StatusCategoryButton.qml diff --git a/ui/shared/status/StatusChatInput.qml b/ui/shared/status/StatusChatInput.qml index 9a66b048c1..f2f6b89b2f 100644 --- a/ui/shared/status/StatusChatInput.qml +++ b/ui/shared/status/StatusChatInput.qml @@ -1088,7 +1088,7 @@ Rectangle { StatusIconButton { id: gifBtn - visible: appSettings.isGifWidgetEnabled + visible: !isEdit && appSettings.isGifWidgetEnabled anchors.right: emojiBtn.left anchors.rightMargin: 2 anchors.bottom: parent.bottom diff --git a/ui/shared/status/StatusEmojiPopup.qml b/ui/shared/status/StatusEmojiPopup.qml index 94695ff50e..b3ede593ff 100644 --- a/ui/shared/status/StatusEmojiPopup.qml +++ b/ui/shared/status/StatusEmojiPopup.qml @@ -336,7 +336,7 @@ Popup { Repeater { model: EmojiJSON.emojiCategories - StatusEmojiCategoryButton { + StatusCategoryButton { source: `../../app/img/emojiCategories/${modelData}.svg` active: index === scrollView.activeCategory changeCategory: function () { diff --git a/ui/shared/status/StatusGifColumn.qml b/ui/shared/status/StatusGifColumn.qml index 90da54f774..a1fbe4607e 100644 --- a/ui/shared/status/StatusGifColumn.qml +++ b/ui/shared/status/StatusGifColumn.qml @@ -11,6 +11,7 @@ Column { property alias gifList: repeater property var gifWidth: 0 property var gifSelected: function () {} + property var toggleFavorite: function () {} Repeater { id: repeater @@ -34,12 +35,45 @@ Column { } } + StatusIconButton { + id: starButton + icon.name: "star-icon" + iconColor: { + if (model.isFavorite) { + return Style.current.yellow + } + return Style.current.secondaryText + } + hoveredIconColor: { + if (iconColor === Style.current.yellow) { + return Style.current.secondaryText + } + return Style.current.yellow + } + highlightedBackgroundOpacity: 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + width: 24 + height: 24 + z: 1 + padding: 10 + onClicked: { + root.toggleFavorite(model) + if (starButton.iconColor === Style.current.yellow) { + starButton.iconColor = Style.current.secondaryText + } else { + starButton.iconColor = Style.current.yellow + } + } + } + AnimatedImage { id: animation visible: animation.status == Image.Ready source: model.tinyUrl width: root.gifWidth fillMode: Image.PreserveAspectFit + z: 0 layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { @@ -53,9 +87,13 @@ Column { } MouseArea { + id: mouseArea + cursorShape: Qt.PointingHandCursor anchors.fill: parent + hoverEnabled: true onClicked: function (event) { root.gifSelected(event, model.url) + chatsModel.gif.addToRecents(model.id) } } } diff --git a/ui/shared/status/StatusGifPopup.qml b/ui/shared/status/StatusGifPopup.qml index a470286508..1451988665 100644 --- a/ui/shared/status/StatusGifPopup.qml +++ b/ui/shared/status/StatusGifPopup.qml @@ -7,14 +7,37 @@ import "../../imports" import "../../shared" Popup { + enum Category { + Trending, + Recent, + Favorite, + Search + } + 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 var toggleCategory: function(newCategory) { + previousCategory = currentCategory + currentCategory = newCategory + searchBox.text = "" + if (currentCategory === StatusGifPopup.Category.Trending) { + chatsModel.gif.getTrendings() + } else if(currentCategory === StatusGifPopup.Category.Favorite) { + chatsModel.gif.getFavorites() + } else if(currentCategory === StatusGifPopup.Category.Recent) { + chatsModel.gif.getRecents() + } + } + property var toggleFavorite: function(item) { + chatsModel.gif.toggleFavorite(item.id, currentCategory === StatusGifPopup.Category.Favorite) + } property alias searchString: searchBox.text + property int currentCategory: StatusGifPopup.Category.Trending + property int previousCategory: StatusGifPopup.Category.Trending + modal: false width: 360 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent @@ -38,7 +61,7 @@ Popup { searchBox.text = "" searchBox.forceActiveFocus(Qt.MouseFocusReason) if (appSettings.isTenorWarningAccepted) { - chatsModel.gif.load() + chatsModel.gif.getTrendings() } else { confirmationPopup.open() } @@ -50,13 +73,6 @@ Popup { } } - Connections { - target: chatsModel.gif - onDataLoaded: { - loading = false - } - } - contentItem: ColumnLayout { anchors.fill: parent spacing: 0 @@ -70,6 +86,8 @@ Popup { SearchBox { id: searchBox + placeholderText: qsTr("Search Tenor") + enabled: appSettings.isTenorWarningAccepted anchors.right: parent.right anchors.rightMargin: gifHeader.headerMargin anchors.top: parent.top @@ -77,13 +95,46 @@ Popup { anchors.left: parent.left anchors.leftMargin: gifHeader.headerMargin Keys.onReleased: { + if (searchBox.text === "") { + toggleCategory(previousCategory) + return + } + if (popup.currentCategory !== StatusGifPopup.Category.Search) { + popup.previousCategory = popup.currentCategory + popup.currentCategory = StatusGifPopup.Category.Search + } Qt.callLater(searchGif, searchBox.text); } } + + StatusIconButton { + id: clearBtn + icon.name: "close-icon" + type: "secondary" + visible: searchBox.text !== "" + icon.width: 14 + icon.height: 14 + width: 14 + height: 14 + anchors.right: searchBox.right + anchors.rightMargin: Style.current.padding + anchors.verticalCenter: searchBox.verticalCenter + onClicked: toggleCategory(previousCategory) + } } StyledText { - text: qsTr("TRENDING") + id: headerText + text: { + if (currentCategory === StatusGifPopup.Category.Trending) { + return qsTr("TRENDING") + } else if(currentCategory === StatusGifPopup.Category.Favorite) { + return qsTr("FAVORITES") + } else if(currentCategory === StatusGifPopup.Category.Recent) { + return qsTr("RECENT") + } + return "" + } visible: searchBox.text === "" color: Style.current.secondaryText font.pixelSize: 13 @@ -94,9 +145,46 @@ Popup { Loader { Layout.fillWidth: true Layout.rightMargin: Style.current.smallPadding / 2 + Layout.leftMargin: Style.current.smallPadding / 2 Layout.alignment: Qt.AlignTop | Qt.AlignLeft - Layout.preferredHeight: 400 - gifHeader.height - sourceComponent: gifItems + Layout.preferredHeight: { + const headerTextHeight = searchBox.text === "" ? headerText.height : 0 + return 400 - gifHeader.height - headerTextHeight + } + sourceComponent: chatsModel.gif.columnA.rowCount() == 0 ? empty : gifItems + } + + Row { + id: categorySelector + Layout.fillWidth: true + leftPadding: Style.current.smallPadding / 2 + rightPadding: Style.current.smallPadding / 2 + spacing: 0 + + StatusCategoryButton { + source: `../../app/img/gifCategories/trending.svg` + active: StatusGifPopup.Category.Trending === popup.currentCategory + changeCategory: function () { + toggleCategory(StatusGifPopup.Category.Trending) + } + enabled: appSettings.isTenorWarningAccepted + } + StatusCategoryButton { + source: `../../app/img/gifCategories/recent.svg` + active: StatusGifPopup.Category.Recent === popup.currentCategory + changeCategory: function () { + toggleCategory(StatusGifPopup.Category.Recent) + } + enabled: appSettings.isTenorWarningAccepted + } + StatusCategoryButton { + source: `../../app/img/gifCategories/favorite.svg` + active: StatusGifPopup.Category.Favorite === popup.currentCategory + changeCategory: function () { + toggleCategory(StatusGifPopup.Category.Favorite) + } + enabled: appSettings.isTenorWarningAccepted + } } } @@ -130,7 +218,7 @@ Popup { SVGImage { id: gifImage anchors.horizontalCenter: parent.horizontalCenter - source: "./assets/img/gif.svg" + source: `./assets/img/gif-${Style.current.name}.svg` } StyledText { @@ -159,7 +247,7 @@ Popup { text: qsTrId("Enable") onClicked: { appSettings.isTenorWarningAccepted = true - chatsModel.gif.load() + chatsModel.gif.getTrendings() confirmationPopup.close() } } @@ -167,8 +255,30 @@ Popup { } Component { - id: gifItems + id: empty + Rectangle { + height: parent.height + width: parent.width + + StyledText { + anchors.centerIn: parent + text: { + if(currentCategory === StatusGifPopup.Category.Favorite) { + return qsTr("Favorite GIFs will appear here") + } else if(currentCategory === StatusGifPopup.Category.Recent) { + return qsTr("Recent GIFs will appear here") + } + return "" + } + font.pixelSize: 15 + color: Style.current.secondaryText + } + } + } + + Component { + id: gifItems ScrollView { id: scrollView property ScrollBar vScrollBar: ScrollBar.vertical @@ -188,18 +298,21 @@ Popup { gifList.model: chatsModel.gif.columnA gifWidth: (popup.width / 3) - Style.current.padding gifSelected: popup.gifSelected + toggleFavorite: popup.toggleFavorite } StatusGifColumn { gifList.model: chatsModel.gif.columnB gifWidth: (popup.width / 3) - Style.current.padding gifSelected: popup.gifSelected + toggleFavorite: popup.toggleFavorite } StatusGifColumn { gifList.model: chatsModel.gif.columnC gifWidth: (popup.width / 3) - Style.current.padding gifSelected: popup.gifSelected + toggleFavorite: popup.toggleFavorite } } diff --git a/ui/shared/status/assets/img/gif.svg b/ui/shared/status/assets/img/gif-light.svg similarity index 100% rename from ui/shared/status/assets/img/gif.svg rename to ui/shared/status/assets/img/gif-light.svg diff --git a/ui/shared/status/qmldir b/ui/shared/status/qmldir index 5e31ac98c4..8ed1818b62 100644 --- a/ui/shared/status/qmldir +++ b/ui/shared/status/qmldir @@ -2,7 +2,7 @@ StatusButton 1.0 StatusButton.qml StatusChatCommandButton 1.0 StatusChatCommandButton.qml StatusChatCommandPopup 1.0 StatusChatCommandPopup.qml StatusChatInput 1.0 StatusChatInput.qml -StatusEmojiCategoryButton 1.0 StatusEmojiCategoryButton.qml +StatusCategoryButton 1.0 StatusCategoryButton.qml StatusEmojiPopup 1.0 StatusEmojiPopup.qml StatusEmojiSection 1.0 StatusEmojiSection.qml StatusGifPopup 1.0 StatusGifPopup.qml diff --git a/vendor/status-go b/vendor/status-go index c4a71f813a..ef014e25e2 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit c4a71f813a783e310b1c8ca59d2390a8d76f6a0c +Subproject commit ef014e25e2e4202cbeab8948f0c8f9f65f7806ee