mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Implement recent reaction userpics in groups.
This commit is contained in:
parent
2dec1b72f7
commit
f3e84de5fb
7 changed files with 204 additions and 12 deletions
|
@ -438,19 +438,32 @@ void MessageReactions::add(const QString &reaction) {
|
||||||
if (_chosen == reaction) {
|
if (_chosen == reaction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auto history = _item->history();
|
||||||
|
const auto self = history->session().user();
|
||||||
if (!_chosen.isEmpty()) {
|
if (!_chosen.isEmpty()) {
|
||||||
const auto i = _list.find(_chosen);
|
const auto i = _list.find(_chosen);
|
||||||
Assert(i != end(_list));
|
Assert(i != end(_list));
|
||||||
--i->second;
|
--i->second;
|
||||||
if (!i->second) {
|
const auto removed = !i->second;
|
||||||
|
if (removed) {
|
||||||
_list.erase(i);
|
_list.erase(i);
|
||||||
}
|
}
|
||||||
|
const auto j = _recent.find(_chosen);
|
||||||
|
if (j != end(_recent)) {
|
||||||
|
j->second.erase(ranges::remove(j->second, self), end(j->second));
|
||||||
|
if (j->second.empty() || removed) {
|
||||||
|
_recent.erase(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_chosen = reaction;
|
_chosen = reaction;
|
||||||
if (!reaction.isEmpty()) {
|
if (!reaction.isEmpty()) {
|
||||||
|
if (_item->canViewReactions()) {
|
||||||
|
_recent[reaction].push_back(self);
|
||||||
|
}
|
||||||
++_list[reaction];
|
++_list[reaction];
|
||||||
}
|
}
|
||||||
auto &owner = _item->history()->owner();
|
auto &owner = history->owner();
|
||||||
owner.reactions().send(_item, _chosen);
|
owner.reactions().send(_item, _chosen);
|
||||||
owner.notifyItemDataChange(_item);
|
owner.notifyItemDataChange(_item);
|
||||||
}
|
}
|
||||||
|
@ -461,8 +474,10 @@ void MessageReactions::remove() {
|
||||||
|
|
||||||
void MessageReactions::set(
|
void MessageReactions::set(
|
||||||
const QVector<MTPReactionCount> &list,
|
const QVector<MTPReactionCount> &list,
|
||||||
|
const QVector<MTPMessageUserReaction> &recent,
|
||||||
bool ignoreChosen) {
|
bool ignoreChosen) {
|
||||||
if (_item->history()->owner().reactions().sending(_item)) {
|
auto &owner = _item->history()->owner();
|
||||||
|
if (owner.reactions().sending(_item)) {
|
||||||
// We'll apply non-stale data from the request response.
|
// We'll apply non-stale data from the request response.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -499,8 +514,24 @@ void MessageReactions::set(
|
||||||
_chosen = QString();
|
_chosen = QString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
auto parsed = base::flat_map<
|
||||||
|
QString,
|
||||||
|
std::vector<not_null<UserData*>>>();
|
||||||
|
for (const auto &reaction : recent) {
|
||||||
|
reaction.match([&](const MTPDmessageUserReaction &data) {
|
||||||
|
const auto emoji = qs(data.vreaction());
|
||||||
|
if (_list.contains(emoji)) {
|
||||||
|
parsed[emoji].push_back(owner.user(data.vuser_id()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_recent != parsed) {
|
||||||
|
_recent = std::move(parsed);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
_item->history()->owner().notifyItemDataChange(_item);
|
owner.notifyItemDataChange(_item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,6 +539,11 @@ const base::flat_map<QString, int> &MessageReactions::list() const {
|
||||||
return _list;
|
return _list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto MessageReactions::recent() const
|
||||||
|
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> & {
|
||||||
|
return _recent;
|
||||||
|
}
|
||||||
|
|
||||||
bool MessageReactions::empty() const {
|
bool MessageReactions::empty() const {
|
||||||
return _list.empty();
|
return _list.empty();
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,8 +125,13 @@ public:
|
||||||
|
|
||||||
void add(const QString &reaction);
|
void add(const QString &reaction);
|
||||||
void remove();
|
void remove();
|
||||||
void set(const QVector<MTPReactionCount> &list, bool ignoreChosen);
|
void set(
|
||||||
|
const QVector<MTPReactionCount> &list,
|
||||||
|
const QVector<MTPMessageUserReaction> &recent,
|
||||||
|
bool ignoreChosen);
|
||||||
[[nodiscard]] const base::flat_map<QString, int> &list() const;
|
[[nodiscard]] const base::flat_map<QString, int> &list() const;
|
||||||
|
[[nodiscard]] auto recent() const
|
||||||
|
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> &;
|
||||||
[[nodiscard]] QString chosen() const;
|
[[nodiscard]] QString chosen() const;
|
||||||
[[nodiscard]] bool empty() const;
|
[[nodiscard]] bool empty() const;
|
||||||
|
|
||||||
|
@ -135,6 +140,7 @@ private:
|
||||||
|
|
||||||
QString _chosen;
|
QString _chosen;
|
||||||
base::flat_map<QString, int> _list;
|
base::flat_map<QString, int> _list;
|
||||||
|
base::flat_map<QString, std::vector<not_null<UserData*>>> _recent;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -834,7 +834,10 @@ void HistoryItem::updateReactions(const MTPMessageReactions *reactions) {
|
||||||
} else if (!_reactions) {
|
} else if (!_reactions) {
|
||||||
_reactions = std::make_unique<Data::MessageReactions>(this);
|
_reactions = std::make_unique<Data::MessageReactions>(this);
|
||||||
}
|
}
|
||||||
_reactions->set(data.vresults().v, data.is_min());
|
_reactions->set(
|
||||||
|
data.vresults().v,
|
||||||
|
data.vrecent_reactons().value_or_empty(),
|
||||||
|
data.is_min());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -847,6 +850,14 @@ const base::flat_map<QString, int> &HistoryItem::reactions() const {
|
||||||
return _reactions ? _reactions->list() : kEmpty;
|
return _reactions ? _reactions->list() : kEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto HistoryItem::recentReactions() const
|
||||||
|
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> & {
|
||||||
|
static const auto kEmpty = base::flat_map<
|
||||||
|
QString,
|
||||||
|
std::vector<not_null<UserData*>>>();
|
||||||
|
return _reactions ? _reactions->recent() : kEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
bool HistoryItem::canViewReactions() const {
|
bool HistoryItem::canViewReactions() const {
|
||||||
return (_flags & MessageFlag::CanViewReactions)
|
return (_flags & MessageFlag::CanViewReactions)
|
||||||
&& _reactions
|
&& _reactions
|
||||||
|
|
|
@ -359,6 +359,8 @@ public:
|
||||||
void updateReactions(const MTPMessageReactions *reactions);
|
void updateReactions(const MTPMessageReactions *reactions);
|
||||||
void updateReactionsUnknown();
|
void updateReactionsUnknown();
|
||||||
[[nodiscard]] const base::flat_map<QString, int> &reactions() const;
|
[[nodiscard]] const base::flat_map<QString, int> &reactions() const;
|
||||||
|
[[nodiscard]] auto recentReactions() const
|
||||||
|
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> &;
|
||||||
[[nodiscard]] bool canViewReactions() const;
|
[[nodiscard]] bool canViewReactions() const;
|
||||||
[[nodiscard]] QString chosenReaction() const;
|
[[nodiscard]] QString chosenReaction() const;
|
||||||
[[nodiscard]] crl::time lastReactionsRefreshTime() const;
|
[[nodiscard]] crl::time lastReactionsRefreshTime() const;
|
||||||
|
|
|
@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/view/history_view_message.h"
|
#include "history/view/history_view_message.h"
|
||||||
#include "history/view/history_view_cursor_state.h"
|
#include "history/view/history_view_cursor_state.h"
|
||||||
#include "history/view/history_view_react_animation.h"
|
#include "history/view/history_view_react_animation.h"
|
||||||
|
#include "history/view/history_view_group_call_bar.h"
|
||||||
#include "data/data_message_reactions.h"
|
#include "data/data_message_reactions.h"
|
||||||
|
#include "data/data_user.h"
|
||||||
#include "lang/lang_tag.h"
|
#include "lang/lang_tag.h"
|
||||||
#include "ui/chat/chat_style.h"
|
#include "ui/chat/chat_style.h"
|
||||||
#include "styles/style_chat.h"
|
#include "styles/style_chat.h"
|
||||||
|
@ -22,6 +24,7 @@ namespace {
|
||||||
|
|
||||||
constexpr auto kInNonChosenOpacity = 0.12;
|
constexpr auto kInNonChosenOpacity = 0.12;
|
||||||
constexpr auto kOutNonChosenOpacity = 0.18;
|
constexpr auto kOutNonChosenOpacity = 0.18;
|
||||||
|
constexpr auto kMaxRecentUserpics = 3;
|
||||||
|
|
||||||
[[nodiscard]] QColor AdaptChosenServiceFg(QColor serviceBg) {
|
[[nodiscard]] QColor AdaptChosenServiceFg(QColor serviceBg) {
|
||||||
serviceBg.setAlpha(std::max(serviceBg.alpha(), 192));
|
serviceBg.setAlpha(std::max(serviceBg.alpha(), 192));
|
||||||
|
@ -92,7 +95,12 @@ void InlineList::layoutButtons() {
|
||||||
? std::move(*i)
|
? std::move(*i)
|
||||||
: prepareButtonWithEmoji(emoji));
|
: prepareButtonWithEmoji(emoji));
|
||||||
const auto add = (emoji == _data.chosenReaction) ? 1 : 0;
|
const auto add = (emoji == _data.chosenReaction) ? 1 : 0;
|
||||||
setButtonCount(buttons.back(), count + add);
|
const auto j = _data.recent.find(emoji);
|
||||||
|
if (j != end(_data.recent) && !j->second.empty()) {
|
||||||
|
setButtonUserpics(buttons.back(), j->second);
|
||||||
|
} else {
|
||||||
|
setButtonCount(buttons.back(), count + add);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_buttons = std::move(buttons);
|
_buttons = std::move(buttons);
|
||||||
}
|
}
|
||||||
|
@ -104,14 +112,53 @@ InlineList::Button InlineList::prepareButtonWithEmoji(const QString &emoji) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void InlineList::setButtonCount(Button &button, int count) {
|
void InlineList::setButtonCount(Button &button, int count) {
|
||||||
if (button.count == count) {
|
if (button.count == count && !button.userpics) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
button.userpics = nullptr;
|
||||||
button.count = count;
|
button.count = count;
|
||||||
button.countText = Lang::FormatCountToShort(count).string;
|
button.countText = Lang::FormatCountToShort(count).string;
|
||||||
button.countTextWidth = st::semiboldFont->width(button.countText);
|
button.countTextWidth = st::semiboldFont->width(button.countText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InlineList::setButtonUserpics(
|
||||||
|
Button &button,
|
||||||
|
const std::vector<not_null<UserData*>> &users) {
|
||||||
|
if (!button.userpics) {
|
||||||
|
button.userpics = std::make_unique<Userpics>();
|
||||||
|
}
|
||||||
|
const auto count = int(users.size());
|
||||||
|
auto &list = button.userpics->list;
|
||||||
|
const auto regenerate = [&] {
|
||||||
|
if (list.size() != count) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (auto i = 0; i != count; ++i) {
|
||||||
|
if (users[i] != list[i].peer) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
if (!regenerate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto generated = std::vector<UserpicInRow>();
|
||||||
|
generated.reserve(count);
|
||||||
|
for (auto i = 0; i != count; ++i) {
|
||||||
|
if (i == list.size()) {
|
||||||
|
list.push_back(UserpicInRow{
|
||||||
|
users[i]
|
||||||
|
});
|
||||||
|
} else if (list[i].peer != users[i]) {
|
||||||
|
list[i].peer = users[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (list.size() > count) {
|
||||||
|
list.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QSize InlineList::countOptimalSize() {
|
QSize InlineList::countOptimalSize() {
|
||||||
if (_buttons.empty()) {
|
if (_buttons.empty()) {
|
||||||
return _skipBlock;
|
return _skipBlock;
|
||||||
|
@ -123,13 +170,26 @@ QSize InlineList::countOptimalSize() {
|
||||||
const auto between = st::reactionInlineBetween;
|
const auto between = st::reactionInlineBetween;
|
||||||
const auto padding = st::reactionInlinePadding;
|
const auto padding = st::reactionInlinePadding;
|
||||||
const auto size = st::reactionInlineSize;
|
const auto size = st::reactionInlineSize;
|
||||||
const auto widthBase = padding.left()
|
const auto widthBaseCount = padding.left()
|
||||||
+ size
|
+ size
|
||||||
+ st::reactionInlineSkip
|
+ st::reactionInlineSkip
|
||||||
+ padding.right();
|
+ padding.right();
|
||||||
|
const auto widthBaseUserpics = padding.left()
|
||||||
|
+ size
|
||||||
|
+ st::reactionInlineUserpicsPadding.left()
|
||||||
|
+ st::reactionInlineUserpicsPadding.right();
|
||||||
|
const auto userpicsWidth = [](const Button &button) {
|
||||||
|
const auto count = int(button.userpics->list.size());
|
||||||
|
const auto single = st::reactionInlineUserpics.size;
|
||||||
|
const auto shift = st::reactionInlineUserpics.shift;
|
||||||
|
const auto width = single + (count - 1) * (single - shift);
|
||||||
|
return width;
|
||||||
|
};
|
||||||
const auto height = padding.top() + size + padding.bottom();
|
const auto height = padding.top() + size + padding.bottom();
|
||||||
for (auto &button : _buttons) {
|
for (auto &button : _buttons) {
|
||||||
const auto width = widthBase + button.countTextWidth;
|
const auto width = button.userpics
|
||||||
|
? (widthBaseUserpics + userpicsWidth(button))
|
||||||
|
: (widthBaseCount + button.countTextWidth);
|
||||||
button.geometry.setSize({ width, height });
|
button.geometry.setSize({ width, height });
|
||||||
x += width + between;
|
x += width + between;
|
||||||
}
|
}
|
||||||
|
@ -243,7 +303,16 @@ void InlineList::paint(
|
||||||
return _animation->paintGetArea(p, QPoint(), image);
|
return _animation->paintGetArea(p, QPoint(), image);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!skipBubble) {
|
if (skipBubble) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resolveUserpicsImage(button);
|
||||||
|
if (button.userpics) {
|
||||||
|
p.drawImage(
|
||||||
|
inner.x() + size + st::reactionInlineUserpicsPadding.left(),
|
||||||
|
geometry.y() + st::reactionInlineUserpicsPadding.top(),
|
||||||
|
button.userpics->image);
|
||||||
|
} else {
|
||||||
p.setPen(!inbubble
|
p.setPen(!inbubble
|
||||||
? (chosen
|
? (chosen
|
||||||
? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))
|
? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))
|
||||||
|
@ -299,6 +368,35 @@ void InlineList::animateSend(
|
||||||
st::reactionInlineImage);
|
st::reactionInlineImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InlineList::resolveUserpicsImage(const Button &button) const {
|
||||||
|
const auto userpics = button.userpics.get();
|
||||||
|
const auto regenerate = [&] {
|
||||||
|
if (!userpics) {
|
||||||
|
return false;
|
||||||
|
} else if (userpics->image.isNull()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (auto &entry : userpics->list) {
|
||||||
|
const auto peer = entry.peer;
|
||||||
|
auto &view = entry.view;
|
||||||
|
const auto wasView = view.get();
|
||||||
|
if (peer->userpicUniqueKey(view) != entry.uniqueKey
|
||||||
|
|| view.get() != wasView) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
if (!regenerate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GenerateUserpicsInRow(
|
||||||
|
userpics->image,
|
||||||
|
userpics->list,
|
||||||
|
st::reactionInlineUserpics,
|
||||||
|
kMaxRecentUserpics);
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<SendAnimation> InlineList::takeSendAnimation() {
|
std::unique_ptr<SendAnimation> InlineList::takeSendAnimation() {
|
||||||
return std::move(_animation);
|
return std::move(_animation);
|
||||||
}
|
}
|
||||||
|
@ -311,9 +409,28 @@ void InlineList::continueSendAnimation(
|
||||||
InlineListData InlineListDataFromMessage(not_null<Message*> message) {
|
InlineListData InlineListDataFromMessage(not_null<Message*> message) {
|
||||||
using Flag = InlineListData::Flag;
|
using Flag = InlineListData::Flag;
|
||||||
const auto item = message->message();
|
const auto item = message->message();
|
||||||
|
|
||||||
auto result = InlineListData();
|
auto result = InlineListData();
|
||||||
result.reactions = item->reactions();
|
result.reactions = item->reactions();
|
||||||
|
const auto &recent = item->recentReactions();
|
||||||
|
const auto showUserpics = [&] {
|
||||||
|
if (recent.size() != result.reactions.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto b = begin(recent);
|
||||||
|
auto sum = 0;
|
||||||
|
for (const auto &[emoji, count] : result.reactions) {
|
||||||
|
sum += count;
|
||||||
|
if (emoji != b->first
|
||||||
|
|| count != b->second.size()
|
||||||
|
|| sum > kMaxRecentUserpics) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
|
if (showUserpics) {
|
||||||
|
result.recent = recent;
|
||||||
|
}
|
||||||
result.chosenReaction = item->chosenReaction();
|
result.chosenReaction = item->chosenReaction();
|
||||||
if (!result.chosenReaction.isEmpty()) {
|
if (!result.chosenReaction.isEmpty()) {
|
||||||
--result.reactions[result.chosenReaction];
|
--result.reactions[result.chosenReaction];
|
||||||
|
|
|
@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
class Reactions;
|
class Reactions;
|
||||||
|
class CloudImageView;
|
||||||
} // namespace Data
|
} // namespace Data
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
|
@ -22,6 +23,7 @@ using PaintContext = Ui::ChatPaintContext;
|
||||||
class Message;
|
class Message;
|
||||||
struct TextState;
|
struct TextState;
|
||||||
struct SendReactionAnimationArgs;
|
struct SendReactionAnimationArgs;
|
||||||
|
struct UserpicInRow;
|
||||||
} // namespace HistoryView
|
} // namespace HistoryView
|
||||||
|
|
||||||
namespace HistoryView::Reactions {
|
namespace HistoryView::Reactions {
|
||||||
|
@ -37,6 +39,7 @@ struct InlineListData {
|
||||||
using Flags = base::flags<Flag>;
|
using Flags = base::flags<Flag>;
|
||||||
|
|
||||||
base::flat_map<QString, int> reactions;
|
base::flat_map<QString, int> reactions;
|
||||||
|
base::flat_map<QString, std::vector<not_null<UserData*>>> recent;
|
||||||
QString chosenReaction;
|
QString chosenReaction;
|
||||||
Flags flags = {};
|
Flags flags = {};
|
||||||
};
|
};
|
||||||
|
@ -74,10 +77,16 @@ public:
|
||||||
void continueSendAnimation(std::unique_ptr<SendAnimation> animation);
|
void continueSendAnimation(std::unique_ptr<SendAnimation> animation);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct Userpics {
|
||||||
|
QImage image;
|
||||||
|
std::vector<UserpicInRow> list;
|
||||||
|
bool someNotLoaded = false;
|
||||||
|
};
|
||||||
struct Button {
|
struct Button {
|
||||||
QRect geometry;
|
QRect geometry;
|
||||||
mutable QImage image;
|
mutable QImage image;
|
||||||
mutable ClickHandlerPtr link;
|
mutable ClickHandlerPtr link;
|
||||||
|
std::unique_ptr<Userpics> userpics;
|
||||||
QString emoji;
|
QString emoji;
|
||||||
QString countText;
|
QString countText;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
@ -88,7 +97,11 @@ private:
|
||||||
void layoutButtons();
|
void layoutButtons();
|
||||||
|
|
||||||
void setButtonCount(Button &button, int count);
|
void setButtonCount(Button &button, int count);
|
||||||
|
void setButtonUserpics(
|
||||||
|
Button &button,
|
||||||
|
const std::vector<not_null<UserData*>> &users);
|
||||||
[[nodiscard]] Button prepareButtonWithEmoji(const QString &emoji);
|
[[nodiscard]] Button prepareButtonWithEmoji(const QString &emoji);
|
||||||
|
void resolveUserpicsImage(const Button &button) const;
|
||||||
|
|
||||||
QSize countOptimalSize() override;
|
QSize countOptimalSize() override;
|
||||||
|
|
||||||
|
|
|
@ -969,6 +969,13 @@ reactionInlineImage: 28px;
|
||||||
reactionInlineSkip: 3px;
|
reactionInlineSkip: 3px;
|
||||||
reactionInlineBetween: 4px;
|
reactionInlineBetween: 4px;
|
||||||
reactionInlineInBubbleLeft: -3px;
|
reactionInlineInBubbleLeft: -3px;
|
||||||
|
reactionInlineUserpicsPadding: margins(1px, 1px, 1px, 1px);
|
||||||
|
reactionInlineUserpics: GroupCallUserpics {
|
||||||
|
size: 18px;
|
||||||
|
shift: 7px;
|
||||||
|
stroke: 1px;
|
||||||
|
align: align(left);
|
||||||
|
}
|
||||||
|
|
||||||
reactionInfoSize: 15px;
|
reactionInfoSize: 15px;
|
||||||
reactionInfoImage: 30px;
|
reactionInfoImage: 30px;
|
||||||
|
|
Loading…
Add table
Reference in a new issue