status-desktop/ui/imports/shared/status/StatusEmojiPopup.qml
Lukáš Tinkl 19e0be07d8 feat: Upgrade emoji selection to support latest emojis
- upgrade to latest TWEmoji 15.1.0 from
https://github.com/jdecked/twemoji (the original twemoji is abandonware
now)
- create new EmojiJSON metadata from https://github.com/milesj/emojibase
- removed unused 26x26 assets
- add storybook page for StatusEmojiPopup

Fixes #8979
2024-09-03 10:19:54 +02:00

339 lines
11 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import utils 1.0
import shared.panels 1.0
import shared.controls 1.0
StatusDropdown {
id: popup
required property var settings
property var categories: []
property alias searchString: searchBox.text
property var skinColors: ["1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff"]
property string emojiSize: ""
signal emojiSelected(string emoji, bool atCu)
modal: false
width: 360
padding: 0
function containsSkinColor(code) {
return skinColors.some(function (color) {
return code.includes(color)
});
}
function addEmoji(emoji) {
const MAX_EMOJI_NUMBER = 36
const extenstionIndex = emoji.filename.lastIndexOf('.');
let iconCodePoint = emoji.filename
if (extenstionIndex > -1) {
iconCodePoint = iconCodePoint.substring(0, extenstionIndex)
}
// Split the filename to get all the parts and then encode them from hex to utf8
const splitCodePoint = iconCodePoint.split('-')
let codePointParts = []
splitCodePoint.forEach(function (codePoint) {
codePointParts.push(`0x${codePoint}`)
})
const encodedIcon = String.fromCodePoint(...codePointParts);
// Add at the start of the list
let recentEmojis = settings.recentEmojis
if (recentEmojis === undefined) {
recentEmojis = []
}
recentEmojis.unshift(emoji)
// Remove duplicates
recentEmojis = recentEmojis.filter(function (e, index) {
return !recentEmojis.some(function (e2, index2) {
return index2 < index && e2.filename === e.filename
})
})
if (recentEmojis.length > MAX_EMOJI_NUMBER) {
// remove last one
recentEmojis.splice(MAX_EMOJI_NUMBER - 1)
}
emojiSectionsRepeater.itemAt(0).allEmojis = recentEmojis
settings.recentEmojis = recentEmojis
// Adding a space because otherwise, some emojis would fuse since emoji is just a string
popup.emojiSelected(StatusQUtils.Emoji.parse(encodedIcon, popup.emojiSize || undefined) + ' ', true)
popup.close()
}
function populateCategories() {
var categoryNames = {"recent": 0}
var newCategories = [[]]
const emojisWithColors = [
"1f64c",
"1f44f",
"1f44b",
"1f44d",
"1f44e",
"1f44a",
"270a",
"270c",
"1f44c",
"270b",
"1f450",
"1f4aa",
"1f64f",
"261d",
"1f446",
"1f447",
"1f448",
"1f449",
"1f595",
"1f590",
"1f918",
"1f596",
"270d",
"1f485",
"1f442",
"1f443",
"1f476",
"1f466",
"1f467",
"1f468",
"1f469",
"1f471",
"1f474",
"1f475",
"1f472",
"1f473",
"1f46e",
"1f477",
"1f482",
"1f385",
"1f47c",
"1f478",
"1f470",
"1f6b6",
"1f3c3",
"1f483",
"1f647",
"1f481",
"1f645",
"1f646",
"1f64b",
"1f64e",
"1f64d",
"1f487",
"1f486",
"1f6a3",
"1f3ca",
"1f3c4",
"1f6c0",
"26f9",
"1f3cb",
"1f6b4",
"1f6b5",
"1f3c7",
"1f575"
]
StatusQUtils.Emoji.emojiJSON.emoji_json.forEach(function (emoji) {
if (!categoryNames[emoji.category] && categoryNames[emoji.category] !== 0) {
categoryNames[emoji.category] = newCategories.length
newCategories.push([])
}
if (settings.skinColor !== "") {
if (emoji.unicode.includes(settings.skinColor)) {
newCategories[categoryNames[emoji.category]].push(Object.assign({}, emoji, {filename: emoji.unicode}));
} else {
if (!emojisWithColors.includes(emoji.unicode) && !containsSkinColor(emoji.unicode)) {
newCategories[categoryNames[emoji.category]].push(Object.assign({}, emoji, {filename: emoji.unicode}));
}
}
} else {
if (!containsSkinColor(emoji.unicode)) {
newCategories[categoryNames[emoji.category]].push(Object.assign({}, emoji, {filename: emoji.unicode}));
}
}
})
if (newCategories[categoryNames.recent].length === 0) {
newCategories[categoryNames.recent].push({category: "recent", empty: true})
}
categories = newCategories;
const recent = settings.recentEmojis;
if (!!recent) {
emojiSectionsRepeater.itemAt(0).allEmojis = recent;
}
}
onOpened: {
searchBox.text = ""
searchBox.input.edit.forceActiveFocus()
Qt.callLater(populateCategories);
}
onClosed: {
popup.emojiSize = ""
}
contentItem: ColumnLayout {
spacing: Style.current.smallPadding
Item {
readonly property int headerMargin: 8
id: emojiHeader
Layout.fillWidth: true
height: searchBox.height + emojiHeader.headerMargin
SearchBox {
input.edit.objectName: "StatusEmojiPopup_searchBox"
id: searchBox
anchors.right: skinToneEmoji.left
anchors.rightMargin: emojiHeader.headerMargin
anchors.top: parent.top
anchors.topMargin: emojiHeader.headerMargin
anchors.left: parent.left
anchors.leftMargin: emojiHeader.headerMargin
}
Row {
id: skinToneEmoji
property bool expandSkinColorOptions: false
clip: true
width: expandSkinColorOptions ? (22 * skinColorEmojiRepeater.count) : 22
height: 22
opacity: expandSkinColorOptions ? 1.0 : 0.0
Behavior on width { NumberAnimation { duration: 400 } }
Behavior on opacity { NumberAnimation { duration: 200 } }
visible: (opacity > 0.1)
anchors.verticalCenter: searchBox.verticalCenter
anchors.right: parent.right
anchors.rightMargin: emojiHeader.headerMargin
Repeater {
id: skinColorEmojiRepeater
model: ["1f590-1f3fb", "1f590-1f3fc", "1f590-1f3fd", "1f590-1f3fe", "1f590-1f3ff", "1f590"]
delegate: StatusEmoji {
width: 22
height: 22
emojiId: modelData
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
settings.skinColor = (index === 5) ? "" : modelData.split("-")[1];
popup.populateCategories();
skinToneEmoji.expandSkinColorOptions = false;
}
}
}
}
}
StatusEmoji {
width: 22
height: 22
anchors.verticalCenter: searchBox.verticalCenter
anchors.right: parent.right
anchors.rightMargin: emojiHeader.headerMargin
visible: !skinToneEmoji.expandSkinColorOptions
emojiId: "1f590" + ((settings.skinColor !== "" && visible) ? ("-" + settings.skinColor) : "")
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
skinToneEmoji.expandSkinColorOptions = true;
}
}
}
}
StatusScrollView {
readonly property ScrollBar vScrollBar: ScrollBar.vertical
property var categrorySectionHeightRatios: []
property int activeCategory: 0
id: scrollView
padding: Style.current.smallPadding
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
contentWidth: availableWidth
ScrollBar.vertical.onPositionChanged: function () {
if (vScrollBar.position < categrorySectionHeightRatios[scrollView.activeCategory - 1]) {
scrollView.activeCategory--
} else if (vScrollBar.position > categrorySectionHeightRatios[scrollView.activeCategory]) {
scrollView.activeCategory++
}
}
function scrollToCategory(category) {
if (category === 0) {
return vScrollBar.setPosition(0)
}
vScrollBar.setPosition(categrorySectionHeightRatios[category - 1])
}
contentHeight: {
var totalHeight = 0
var categoryHeights = []
for (let i = 0; i < emojiSectionsRepeater.count; i++) {
totalHeight += emojiSectionsRepeater.itemAt(i).height
categoryHeights.push(totalHeight)
}
var ratios = []
categoryHeights.forEach(function (catHeight) {
ratios.push(catHeight / totalHeight)
})
categrorySectionHeightRatios = ratios
return totalHeight - scrollView.topPadding - scrollView.bottomPadding
}
Repeater {
id: emojiSectionsRepeater
model: popup.categories
StatusEmojiSection {
width: scrollView.availableWidth
searchString: popup.searchString
addEmoji: popup.addEmoji
}
}
}
Row {
Layout.fillWidth: true
height: 40
spacing: 0
Repeater {
model: StatusQUtils.Emoji.emojiJSON.emojiCategories
StatusTabBarIconButton {
icon.name: modelData
highlighted: index === scrollView.activeCategory
onClicked: {
scrollView.activeCategory = index
scrollView.scrollToCategory(index)
}
}
}
}
}
}