mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-03 21:54:05 +02:00
Show nice confcall invites.
This commit is contained in:
parent
aaa37a3e0d
commit
09229812f4
14 changed files with 170 additions and 29 deletions
|
@ -4671,18 +4671,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
"lng_call_outgoing" = "Outgoing call";
|
||||
"lng_call_video_outgoing" = "Outgoing video call";
|
||||
"lng_call_group_outgoing" = "Outgoing group call";
|
||||
"lng_call_incoming" = "Incoming call";
|
||||
"lng_call_video_incoming" = "Incoming video call";
|
||||
"lng_call_group_incoming" = "Incoming group call";
|
||||
"lng_call_missed" = "Missed call";
|
||||
"lng_call_video_missed" = "Missed video call";
|
||||
"lng_call_group_missed" = "Missed group call";
|
||||
"lng_call_cancelled" = "Canceled call";
|
||||
"lng_call_video_cancelled" = "Canceled video call";
|
||||
"lng_call_declined" = "Declined call";
|
||||
"lng_call_video_declined" = "Declined video call";
|
||||
"lng_call_group_declined" = "Declined group call";
|
||||
"lng_call_duration_info" = "{time}, {duration}";
|
||||
"lng_call_type_and_duration" = "{type} ({duration})";
|
||||
"lng_call_invitation" = "Call invitation";
|
||||
"lng_call_ongoing" = "Ongoing call";
|
||||
"lng_call_invitation" = "Group call invitation";
|
||||
"lng_call_ongoing" = "Ongoing group call";
|
||||
|
||||
"lng_call_rate_label" = "Please rate the quality of your call";
|
||||
"lng_call_rate_comment" = "Comment (optional)";
|
||||
|
|
|
@ -26,8 +26,10 @@ UserpicButton {
|
|||
}
|
||||
UserpicsRow {
|
||||
button: UserpicButton;
|
||||
bg: color;
|
||||
shift: pixels;
|
||||
stroke: pixels;
|
||||
complex: bool;
|
||||
invert: bool;
|
||||
}
|
||||
ShortInfoBox {
|
||||
|
|
|
@ -694,8 +694,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
bool painting = false;
|
||||
};
|
||||
const auto full = st.button.size.height()
|
||||
+ st::boostReplaceIconAdd.y()
|
||||
+ st::lineWidth;
|
||||
+ (st.complex ? (st::boostReplaceIconAdd.y() + st::lineWidth) : 0);
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
|
||||
const auto raw = result.data();
|
||||
const auto overlay = CreateChild<Ui::RpWidget>(raw);
|
||||
|
@ -731,6 +730,12 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
overlay->update();
|
||||
}, raw->lifetime());
|
||||
|
||||
if (const auto count = state->count.current()) {
|
||||
const auto single = st.button.size.width();
|
||||
const auto used = std::min(count, int(state->buttons.size()));
|
||||
const auto shift = st.shift;
|
||||
raw->resize(used ? (single + (used - 1) * shift) : 0, raw->height());
|
||||
}
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
state->count.value()
|
||||
|
@ -768,7 +773,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
auto hq = PainterHighQualityEnabler(q);
|
||||
const auto stroke = st.stroke;
|
||||
const auto half = stroke / 2.;
|
||||
auto pen = st::windowBg->p;
|
||||
auto pen = st.bg->p;
|
||||
pen.setWidthF(stroke * 2.);
|
||||
state->painting = true;
|
||||
const auto paintOne = [&](not_null<Ui::UserpicButton*> button) {
|
||||
|
@ -788,18 +793,18 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
}
|
||||
}
|
||||
state->painting = false;
|
||||
const auto last = state->buttons.back().get();
|
||||
const auto add = st::boostReplaceIconAdd;
|
||||
const auto skip = st::boostReplaceIconSkip;
|
||||
const auto w = st::boostReplaceIcon.width() + 2 * skip;
|
||||
const auto h = st::boostReplaceIcon.height() + 2 * skip;
|
||||
const auto x = last->x() + last->width() - w + add.x();
|
||||
const auto y = last->y() + last->height() - h + add.y();
|
||||
|
||||
const auto text = (state->count.current() > limit)
|
||||
? ('+' + QString::number(state->count.current() - limit))
|
||||
: QString();
|
||||
if (!text.isEmpty()) {
|
||||
if (st.complex && !text.isEmpty()) {
|
||||
const auto last = state->buttons.back().get();
|
||||
const auto add = st::boostReplaceIconAdd;
|
||||
const auto skip = st::boostReplaceIconSkip;
|
||||
const auto w = st::boostReplaceIcon.width() + 2 * skip;
|
||||
const auto h = st::boostReplaceIcon.height() + 2 * skip;
|
||||
const auto x = last->x() + last->width() - w + add.x();
|
||||
const auto y = last->y() + last->height() - h + add.y();
|
||||
const auto &font = st::semiboldFont;
|
||||
const auto width = font->width(text);
|
||||
const auto padded = std::max(w, width + 2 * font->spacew);
|
||||
|
|
|
@ -37,6 +37,7 @@ CallBodyLayout {
|
|||
photoSize: pixels;
|
||||
nameTop: pixels;
|
||||
statusTop: pixels;
|
||||
participantsTop: pixels;
|
||||
muteStroke: pixels;
|
||||
muteSize: pixels;
|
||||
mutePosition: point;
|
||||
|
@ -48,6 +49,7 @@ callBodyLayout: CallBodyLayout {
|
|||
photoSize: 160px;
|
||||
nameTop: 221px;
|
||||
statusTop: 254px;
|
||||
participantsTop: 294px;
|
||||
muteStroke: 3px;
|
||||
muteSize: 36px;
|
||||
mutePosition: point(142px, 135px);
|
||||
|
@ -58,6 +60,7 @@ callBodyWithPreview: CallBodyLayout {
|
|||
photoSize: 100px;
|
||||
nameTop: 132px;
|
||||
statusTop: 163px;
|
||||
participantsTop: 193px;
|
||||
muteStroke: 3px;
|
||||
muteSize: 0px;
|
||||
mutePosition: point(90px, 84px);
|
||||
|
@ -1538,11 +1541,27 @@ confcallJoinUserpics: UserpicsRow {
|
|||
size: size(36px, 36px);
|
||||
photoSize: 36px;
|
||||
}
|
||||
bg: boxBg;
|
||||
shift: 16px;
|
||||
stroke: 2px;
|
||||
invert: true;
|
||||
}
|
||||
confcallJoinUserpicsPadding: margins(0px, 0px, 0px, 16px);
|
||||
confcallInviteUserpicsBg: groupCallMembersBg;
|
||||
confcallInviteUserpics: UserpicsRow {
|
||||
button: UserpicButton(defaultUserpicButton) {
|
||||
size: size(24px, 24px);
|
||||
photoSize: 24px;
|
||||
}
|
||||
bg: confcallInviteUserpicsBg;
|
||||
shift: 10px;
|
||||
stroke: 2px;
|
||||
invert: true;
|
||||
}
|
||||
confcallInviteParticipants: FlatLabel(defaultFlatLabel) {
|
||||
textFg: callNameFg;
|
||||
}
|
||||
confcallInviteParticipantsPadding: margins(8px, 3px, 12px, 2px);
|
||||
confcallInviteVideo: IconButton {
|
||||
width: 36px;
|
||||
height: 52px;
|
||||
|
|
|
@ -253,6 +253,7 @@ Call::Call(
|
|||
not_null<UserData*> user,
|
||||
CallId conferenceId,
|
||||
MsgId conferenceInviteMsgId,
|
||||
std::vector<not_null<PeerData*>> conferenceParticipants,
|
||||
bool video)
|
||||
: _delegate(delegate)
|
||||
, _user(user)
|
||||
|
@ -279,6 +280,7 @@ Call::Call(
|
|||
, _id(base::RandomValue<CallId>())
|
||||
, _conferenceId(conferenceId)
|
||||
, _conferenceInviteMsgId(conferenceInviteMsgId)
|
||||
, _conferenceParticipants(std::move(conferenceParticipants))
|
||||
, _videoIncoming(
|
||||
std::make_unique<Webrtc::VideoTrack>(
|
||||
StartVideoState(video)))
|
||||
|
|
|
@ -107,6 +107,7 @@ public:
|
|||
not_null<UserData*> user,
|
||||
CallId conferenceId,
|
||||
MsgId conferenceInviteMsgId,
|
||||
std::vector<not_null<PeerData*>> conferenceParticipants,
|
||||
bool video);
|
||||
|
||||
[[nodiscard]] Type type() const {
|
||||
|
@ -127,6 +128,10 @@ public:
|
|||
[[nodiscard]] MsgId conferenceInviteMsgId() const {
|
||||
return _conferenceInviteMsgId;
|
||||
}
|
||||
[[nodiscard]] auto conferenceParticipants() const
|
||||
-> const std::vector<not_null<PeerData*>> & {
|
||||
return _conferenceParticipants;
|
||||
}
|
||||
[[nodiscard]] bool isIncomingWaiting() const;
|
||||
|
||||
void start(bytes::const_span random);
|
||||
|
@ -343,6 +348,7 @@ private:
|
|||
|
||||
CallId _conferenceId = 0;
|
||||
MsgId _conferenceInviteMsgId = 0;
|
||||
std::vector<not_null<PeerData*>> _conferenceParticipants;
|
||||
|
||||
std::unique_ptr<tgcalls::Instance> _instance;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _videoCapture;
|
||||
|
|
|
@ -1024,6 +1024,11 @@ void Instance::showConferenceInvite(
|
|||
return;
|
||||
}
|
||||
|
||||
auto conferenceParticipants = call->otherParticipants;
|
||||
if (!ranges::contains(conferenceParticipants, user)) {
|
||||
conferenceParticipants.push_back(user);
|
||||
}
|
||||
|
||||
const auto &config = user->session().serverConfig();
|
||||
if (inCall() || inGroupCall()) {
|
||||
declineIncomingConferenceInvites(conferenceId);
|
||||
|
@ -1038,6 +1043,7 @@ void Instance::showConferenceInvite(
|
|||
user,
|
||||
conferenceId,
|
||||
conferenceInviteMsgId,
|
||||
std::move(conferenceParticipants),
|
||||
video);
|
||||
const auto raw = call.get();
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "calls/calls_panel.h"
|
||||
|
||||
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
|
@ -215,6 +216,7 @@ Panel::Panel(not_null<Call*> call)
|
|||
initWindow();
|
||||
initWidget();
|
||||
initControls();
|
||||
initConferenceInvite();
|
||||
initLayout();
|
||||
initMediaDeviceToggles();
|
||||
showAndActivate();
|
||||
|
@ -534,6 +536,65 @@ void Panel::initControls() {
|
|||
_screencast->finishAnimating();
|
||||
}
|
||||
|
||||
void Panel::initConferenceInvite() {
|
||||
const auto &participants = _call->conferenceParticipants();
|
||||
const auto count = int(participants.size());
|
||||
if (count < 2) {
|
||||
return;
|
||||
}
|
||||
_conferenceParticipants.create(widget());
|
||||
_conferenceParticipants->show();
|
||||
const auto raw = _conferenceParticipants.data();
|
||||
|
||||
auto peers = std::vector<not_null<PeerData*>>();
|
||||
for (const auto &peer : participants) {
|
||||
if (peer == _user && count > 3) {
|
||||
continue;
|
||||
}
|
||||
peers.push_back(peer);
|
||||
if (peers.size() == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto userpics = CreateUserpicsWithMoreBadge(
|
||||
raw,
|
||||
rpl::single(peers),
|
||||
st::confcallInviteUserpics,
|
||||
peers.size()).release();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
tr::lng_group_call_members(tr::now, lt_count, count),
|
||||
st::confcallInviteParticipants);
|
||||
const auto padding = st::confcallInviteParticipantsPadding;
|
||||
const auto add = padding.bottom();
|
||||
const auto width = add
|
||||
+ userpics->width()
|
||||
+ padding.left()
|
||||
+ label->width()
|
||||
+ padding.right();
|
||||
const auto height = add + userpics->height() + add;
|
||||
|
||||
_status->geometryValue() | rpl::start_with_next([=] {
|
||||
const auto top = _bodyTop + _bodySt->participantsTop;
|
||||
const auto left = (widget()->width() - width) / 2;
|
||||
raw->setGeometry(left, top, width, height);
|
||||
userpics->move(add, add);
|
||||
label->move(add + userpics->width() + padding.left(), padding.top());
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto radius = raw->height() / 2.;
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::confcallInviteUserpicsBg);
|
||||
p.drawRoundedRect(raw->rect(), radius, radius);
|
||||
}, raw->lifetime());
|
||||
}
|
||||
|
||||
void Panel::setIncomingSize(QSize size) {
|
||||
if (_incomingFrameSize == size) {
|
||||
return;
|
||||
|
@ -1160,7 +1221,11 @@ void Panel::updateControlsGeometry() {
|
|||
std::min(
|
||||
bodyPreviewSizeMax.height(),
|
||||
st::callOutgoingPreviewMax.height()));
|
||||
const auto contentHeight = _bodySt->height
|
||||
const auto bodyContentHeight = _bodySt->height
|
||||
+ (_conferenceParticipants
|
||||
? (_bodySt->participantsTop - _bodySt->statusTop)
|
||||
: 0);
|
||||
const auto contentHeight = bodyContentHeight
|
||||
+ (_outgoingPreviewInBody ? bodyPreviewSize.height() : 0);
|
||||
const auto remainingHeight = available - contentHeight;
|
||||
const auto skipHeight = remainingHeight
|
||||
|
@ -1172,7 +1237,7 @@ void Panel::updateControlsGeometry() {
|
|||
widget()->height(),
|
||||
_buttonsTopShown,
|
||||
shown);
|
||||
const auto previewTop = _bodyTop + _bodySt->height + skipHeight;
|
||||
const auto previewTop = _bodyTop + bodyContentHeight + skipHeight;
|
||||
|
||||
_userpic->setGeometry(
|
||||
(widget()->width() - _bodySt->photoSize) / 2,
|
||||
|
|
|
@ -142,6 +142,7 @@ private:
|
|||
void initWindow();
|
||||
void initWidget();
|
||||
void initControls();
|
||||
void initConferenceInvite();
|
||||
void reinitWithCall(Call *call);
|
||||
void initLayout();
|
||||
void initMediaDeviceToggles();
|
||||
|
@ -211,6 +212,7 @@ private:
|
|||
object_ptr < Ui::FadeWrap<Ui::CallButton>> _addPeople;
|
||||
object_ptr<Ui::FlatLabel> _name;
|
||||
object_ptr<Ui::FlatLabel> _status;
|
||||
object_ptr<Ui::RpWidget> _conferenceParticipants = { nullptr };
|
||||
object_ptr<Ui::RpWidget> _fingerprint = { nullptr };
|
||||
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute = { nullptr };
|
||||
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteLowBattery
|
||||
|
|
|
@ -456,7 +456,9 @@ Invoice ComputeInvoiceData(
|
|||
return result;
|
||||
}
|
||||
|
||||
Call ComputeCallData(const MTPDmessageActionPhoneCall &call) {
|
||||
Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionPhoneCall &call) {
|
||||
auto result = Call();
|
||||
result.state = [&] {
|
||||
if (const auto reason = call.vreason()) {
|
||||
|
@ -480,8 +482,18 @@ Call ComputeCallData(const MTPDmessageActionPhoneCall &call) {
|
|||
return result;
|
||||
}
|
||||
|
||||
Call ComputeCallData(const MTPDmessageActionConferenceCall &call) {
|
||||
Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionConferenceCall &call) {
|
||||
auto participants = std::vector<not_null<PeerData*>>();
|
||||
if (const auto list = call.vother_participants()) {
|
||||
participants.reserve(list->v.size());
|
||||
for (const auto &participant : list->v) {
|
||||
participants.push_back(owner->peer(peerFromMTP(participant)));
|
||||
}
|
||||
}
|
||||
return {
|
||||
.otherParticipants = std::move(participants),
|
||||
.conferenceId = call.vcall_id().v,
|
||||
.duration = call.vduration().value_or_empty(),
|
||||
.state = (call.vduration().value_or_empty()
|
||||
|
@ -1723,7 +1735,8 @@ const Call *MediaCall::call() const {
|
|||
}
|
||||
|
||||
TextWithEntities MediaCall::notificationText() const {
|
||||
auto result = Text(parent(), _call.state, _call.video);
|
||||
const auto conference = (_call.conferenceId != 0);
|
||||
auto result = Text(parent(), _call.state, conference, _call.video);
|
||||
if (_call.duration > 0) {
|
||||
result = tr::lng_call_type_and_duration(
|
||||
tr::now,
|
||||
|
@ -1765,6 +1778,7 @@ std::unique_ptr<HistoryView::Media> MediaCall::createView(
|
|||
QString MediaCall::Text(
|
||||
not_null<HistoryItem*> item,
|
||||
CallState state,
|
||||
bool conference,
|
||||
bool video) {
|
||||
if (state == CallState::Invitation) {
|
||||
return tr::lng_call_invitation(tr::now);
|
||||
|
@ -1772,14 +1786,20 @@ QString MediaCall::Text(
|
|||
return tr::lng_call_ongoing(tr::now);
|
||||
} else if (item->out()) {
|
||||
return ((state == CallState::Missed)
|
||||
? (video
|
||||
? (conference
|
||||
? tr::lng_call_group_declined
|
||||
: video
|
||||
? tr::lng_call_video_cancelled
|
||||
: tr::lng_call_cancelled)
|
||||
: (video
|
||||
: (conference
|
||||
? tr::lng_call_group_outgoing
|
||||
: video
|
||||
? tr::lng_call_video_outgoing
|
||||
: tr::lng_call_outgoing))(tr::now);
|
||||
} else if (state == CallState::Missed) {
|
||||
return (video
|
||||
return (conference
|
||||
? tr::lng_call_group_missed
|
||||
: video
|
||||
? tr::lng_call_video_missed
|
||||
: tr::lng_call_missed)(tr::now);
|
||||
} else if (state == CallState::Busy) {
|
||||
|
@ -1787,7 +1807,9 @@ QString MediaCall::Text(
|
|||
? tr::lng_call_video_declined
|
||||
: tr::lng_call_declined)(tr::now);
|
||||
}
|
||||
return (video
|
||||
return (conference
|
||||
? tr::lng_call_group_incoming
|
||||
: video
|
||||
? tr::lng_call_video_incoming
|
||||
: tr::lng_call_incoming)(tr::now);
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ struct SharedContact final {
|
|||
struct Call {
|
||||
using State = CallState;
|
||||
|
||||
std::vector<not_null<PeerData*>> otherParticipants;
|
||||
CallId conferenceId = 0;
|
||||
int duration = 0;
|
||||
State state = State::Missed;
|
||||
|
@ -468,6 +469,7 @@ public:
|
|||
[[nodiscard]] static QString Text(
|
||||
not_null<HistoryItem*> item,
|
||||
CallState state,
|
||||
bool conference,
|
||||
bool video);
|
||||
|
||||
private:
|
||||
|
@ -801,8 +803,11 @@ private:
|
|||
not_null<HistoryItem*> item,
|
||||
const MTPDmessageMediaPaidMedia &data);
|
||||
|
||||
[[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call);
|
||||
[[nodiscard]] Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionPhoneCall &call);
|
||||
[[nodiscard]] Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionConferenceCall &call);
|
||||
|
||||
[[nodiscard]] GiveawayStart ComputeGiveawayStartData(
|
||||
|
|
|
@ -481,13 +481,13 @@ HistoryItem::HistoryItem(
|
|||
createComponents(CreateConfig());
|
||||
_media = std::make_unique<Data::MediaCall>(
|
||||
this,
|
||||
Data::ComputeCallData(data));
|
||||
Data::ComputeCallData(&history->owner(), data));
|
||||
setTextValue({});
|
||||
}, [&](const MTPDmessageActionConferenceCall &data) {
|
||||
createComponents(CreateConfig());
|
||||
_media = std::make_unique<Data::MediaCall>(
|
||||
this,
|
||||
Data::ComputeCallData(data));
|
||||
Data::ComputeCallData(&history->owner(), data));
|
||||
setTextValue({});
|
||||
}, [&](const auto &) {
|
||||
createServiceFromMtp(data);
|
||||
|
@ -1906,6 +1906,7 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) {
|
|||
_media = std::make_unique<Data::MediaCall>(
|
||||
this,
|
||||
Data::ComputeCallData(
|
||||
&history()->owner(),
|
||||
message.vaction().c_messageActionConferenceCall()));
|
||||
addToSharedMediaIndex();
|
||||
finishEdition(-1);
|
||||
|
|
|
@ -47,7 +47,7 @@ Call::Call(
|
|||
, _conference(call->conferenceId != 0)
|
||||
, _video(call->video) {
|
||||
const auto item = parent->data();
|
||||
_text = Data::MediaCall::Text(item, _state, _video);
|
||||
_text = Data::MediaCall::Text(item, _state, _conference, _video);
|
||||
_status = QLocale().toString(
|
||||
parent->dateTime().time(),
|
||||
QLocale::ShortFormat);
|
||||
|
@ -118,10 +118,10 @@ void Call::draw(Painter &p, const PaintContext &context) const {
|
|||
p.setPen(stm->mediaFg);
|
||||
p.drawTextLeft(statusleft, statustop, paintw, _status);
|
||||
|
||||
const auto &icon = _conference
|
||||
? stm->historyCallGroupIcon
|
||||
: _video
|
||||
const auto &icon = _video
|
||||
? stm->historyCallCameraIcon
|
||||
: _conference
|
||||
? stm->historyCallGroupIcon
|
||||
: stm->historyCallIcon;
|
||||
icon.paint(p, paintw - st::historyCallIconPosition.x() - icon.width(), st::historyCallIconPosition.y() - topMinus, paintw);
|
||||
}
|
||||
|
|
|
@ -313,8 +313,10 @@ boostReplaceIconAdd: point(4px, 2px);
|
|||
boostReplaceArrow: icon{{ "mediaview/next", windowSubTextFg }};
|
||||
boostReplaceUserpicsRow: UserpicsRow {
|
||||
button: boostReplaceUserpic;
|
||||
bg: windowBg;
|
||||
shift: boostReplaceUserpicsShift;
|
||||
stroke: boostReplaceIconOutline;
|
||||
complex: true;
|
||||
}
|
||||
|
||||
showOrIconLastSeen: icon{{ "settings/premium/large_lastseen", windowFgActive }};
|
||||
|
|
Loading…
Add table
Reference in a new issue