2019-01-04 11:09:48 +00:00
|
|
|
/*
|
|
|
|
This file is part of Telegram Desktop,
|
|
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
|
|
|
|
For license and copyright information please follow this link:
|
|
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
|
|
*/
|
|
|
|
#include "data/data_channel.h"
|
|
|
|
|
|
|
|
#include "data/data_peer_values.h"
|
|
|
|
#include "data/data_channel_admins.h"
|
|
|
|
#include "data/data_user.h"
|
2019-01-14 06:34:51 +00:00
|
|
|
#include "data/data_chat.h"
|
2019-01-04 11:09:48 +00:00
|
|
|
#include "data/data_session.h"
|
2019-04-15 11:54:03 +00:00
|
|
|
#include "data/data_folder.h"
|
2019-06-21 12:27:46 +00:00
|
|
|
#include "data/data_location.h"
|
2019-07-16 11:46:50 +00:00
|
|
|
#include "base/unixtime.h"
|
2019-04-23 09:40:14 +00:00
|
|
|
#include "history/history.h"
|
2019-01-04 11:09:48 +00:00
|
|
|
#include "observer_peer.h"
|
2019-07-24 11:45:24 +00:00
|
|
|
#include "main/main_session.h"
|
2019-01-13 08:03:34 +00:00
|
|
|
#include "apiwrap.h"
|
2019-01-04 11:09:48 +00:00
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
using UpdateFlag = Notify::PeerUpdate::Flag;
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
2019-01-14 06:34:51 +00:00
|
|
|
ChatData *MegagroupInfo::getMigrateFromChat() const {
|
|
|
|
return _migratedFrom;
|
|
|
|
}
|
|
|
|
|
|
|
|
void MegagroupInfo::setMigrateFromChat(ChatData *chat) {
|
|
|
|
_migratedFrom = chat;
|
|
|
|
}
|
|
|
|
|
2019-06-21 12:27:46 +00:00
|
|
|
const ChannelLocation *MegagroupInfo::getLocation() const {
|
|
|
|
return _location.address.isEmpty() ? nullptr : &_location;
|
|
|
|
}
|
|
|
|
|
|
|
|
void MegagroupInfo::setLocation(const ChannelLocation &location) {
|
|
|
|
_location = location;
|
|
|
|
}
|
|
|
|
|
2019-01-04 11:09:48 +00:00
|
|
|
ChannelData::ChannelData(not_null<Data::Session*> owner, PeerId id)
|
|
|
|
: PeerData(owner, id)
|
|
|
|
, inputChannel(MTP_inputChannel(MTP_int(bareId()), MTP_long(0))) {
|
|
|
|
Data::PeerFlagValue(
|
|
|
|
this,
|
|
|
|
MTPDchannel::Flag::f_megagroup
|
2019-01-14 06:34:51 +00:00
|
|
|
) | rpl::start_with_next([=](bool megagroup) {
|
2019-01-04 11:09:48 +00:00
|
|
|
if (megagroup) {
|
|
|
|
if (!mgInfo) {
|
|
|
|
mgInfo = std::make_unique<MegagroupInfo>();
|
|
|
|
}
|
|
|
|
} else if (mgInfo) {
|
|
|
|
mgInfo = nullptr;
|
|
|
|
}
|
|
|
|
}, _lifetime);
|
2019-01-14 06:34:51 +00:00
|
|
|
|
|
|
|
Data::PeerFlagsValue(
|
|
|
|
this,
|
|
|
|
MTPDchannel::Flag::f_left | MTPDchannel_ClientFlag::f_forbidden
|
|
|
|
) | rpl::distinct_until_changed(
|
|
|
|
) | rpl::start_with_next([=] {
|
|
|
|
if (const auto chat = getMigrateFromChat()) {
|
|
|
|
Notify::peerUpdatedDelayed(chat, UpdateFlag::MigrationChanged);
|
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::MigrationChanged);
|
|
|
|
}
|
|
|
|
}, _lifetime);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setPhoto(const MTPChatPhoto &photo) {
|
|
|
|
setPhoto(userpicPhotoId(), photo);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setPhoto(PhotoId photoId, const MTPChatPhoto &photo) {
|
2019-03-22 14:19:43 +00:00
|
|
|
photo.match([&](const MTPDchatPhoto & data) {
|
2019-07-05 13:38:38 +00:00
|
|
|
updateUserpic(photoId, data.vdc_id().v, data.vphoto_small());
|
2019-03-22 14:19:43 +00:00
|
|
|
}, [&](const MTPDchatPhotoEmpty &) {
|
2019-01-04 11:09:48 +00:00
|
|
|
clearUserpic();
|
2019-03-22 14:19:43 +00:00
|
|
|
});
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setName(const QString &newName, const QString &newUsername) {
|
|
|
|
updateNameDelayed(newName.isEmpty() ? name : newName, QString(), newUsername);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setInviteLink(const QString &newInviteLink) {
|
|
|
|
if (newInviteLink != _inviteLink) {
|
|
|
|
_inviteLink = newInviteLink;
|
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::InviteLinkChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QString ChannelData::inviteLink() const {
|
|
|
|
return _inviteLink;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canHaveInviteLink() const {
|
|
|
|
return (adminRights() & AdminRight::f_invite_users)
|
|
|
|
|| amCreator();
|
|
|
|
}
|
|
|
|
|
2019-06-21 12:27:46 +00:00
|
|
|
void ChannelData::setLocation(const MTPChannelLocation &data) {
|
|
|
|
if (!mgInfo) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto was = mgInfo->getLocation();
|
|
|
|
const auto wasValue = was ? *was : ChannelLocation();
|
|
|
|
data.match([&](const MTPDchannelLocation &data) {
|
2019-07-05 13:38:38 +00:00
|
|
|
data.vgeo_point().match([&](const MTPDgeoPoint &point) {
|
2019-06-21 12:27:46 +00:00
|
|
|
mgInfo->setLocation({
|
2019-07-05 13:38:38 +00:00
|
|
|
qs(data.vaddress()),
|
2019-06-21 12:27:46 +00:00
|
|
|
Data::LocationPoint(point)
|
|
|
|
});
|
|
|
|
}, [&](const MTPDgeoPointEmpty &) {
|
|
|
|
mgInfo->setLocation(ChannelLocation());
|
|
|
|
});
|
|
|
|
}, [&](const MTPDchannelLocationEmpty &) {
|
|
|
|
mgInfo->setLocation(ChannelLocation());
|
|
|
|
});
|
|
|
|
const auto now = mgInfo->getLocation();
|
|
|
|
const auto nowValue = now ? *now : ChannelLocation();
|
|
|
|
if (was != now || (was && wasValue != nowValue)) {
|
|
|
|
Notify::peerUpdatedDelayed(
|
|
|
|
this,
|
|
|
|
Notify::PeerUpdate::Flag::ChannelLocation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const ChannelLocation *ChannelData::getLocation() const {
|
|
|
|
return mgInfo ? mgInfo->getLocation() : nullptr;
|
|
|
|
}
|
|
|
|
|
2019-05-23 21:38:49 +00:00
|
|
|
void ChannelData::setLinkedChat(ChannelData *linked) {
|
|
|
|
if (_linkedChat != linked) {
|
|
|
|
_linkedChat = linked;
|
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::ChannelLinkedChat);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ChannelData *ChannelData::linkedChat() const {
|
|
|
|
return _linkedChat;
|
|
|
|
}
|
|
|
|
|
2019-01-04 11:09:48 +00:00
|
|
|
void ChannelData::setMembersCount(int newMembersCount) {
|
|
|
|
if (_membersCount != newMembersCount) {
|
|
|
|
if (isMegagroup() && !mgInfo->lastParticipants.empty()) {
|
|
|
|
mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated;
|
|
|
|
mgInfo->lastParticipantsCount = membersCount();
|
|
|
|
}
|
|
|
|
_membersCount = newMembersCount;
|
|
|
|
Notify::peerUpdatedDelayed(this, Notify::PeerUpdate::Flag::MembersChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setAdminsCount(int newAdminsCount) {
|
|
|
|
if (_adminsCount != newAdminsCount) {
|
|
|
|
_adminsCount = newAdminsCount;
|
|
|
|
Notify::peerUpdatedDelayed(this, Notify::PeerUpdate::Flag::AdminsChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setRestrictedCount(int newRestrictedCount) {
|
|
|
|
if (_restrictedCount != newRestrictedCount) {
|
|
|
|
_restrictedCount = newRestrictedCount;
|
|
|
|
Notify::peerUpdatedDelayed(this, Notify::PeerUpdate::Flag::BannedUsersChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setKickedCount(int newKickedCount) {
|
|
|
|
if (_kickedCount != newKickedCount) {
|
|
|
|
_kickedCount = newKickedCount;
|
|
|
|
Notify::peerUpdatedDelayed(this, Notify::PeerUpdate::Flag::BannedUsersChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
MTPChatBannedRights ChannelData::KickedRestrictedRights() {
|
|
|
|
using Flag = MTPDchatBannedRights::Flag;
|
2019-01-10 06:26:08 +00:00
|
|
|
const auto flags = Flag::f_view_messages
|
|
|
|
| Flag::f_send_messages
|
|
|
|
| Flag::f_send_media
|
|
|
|
| Flag::f_embed_links
|
|
|
|
| Flag::f_send_stickers
|
|
|
|
| Flag::f_send_gifs
|
|
|
|
| Flag::f_send_games
|
|
|
|
| Flag::f_send_inline;
|
|
|
|
return MTP_chatBannedRights(
|
|
|
|
MTP_flags(flags),
|
|
|
|
MTP_int(std::numeric_limits<int32>::max()));
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
2019-01-10 06:26:08 +00:00
|
|
|
void ChannelData::applyEditAdmin(
|
|
|
|
not_null<UserData*> user,
|
|
|
|
const MTPChatAdminRights &oldRights,
|
2019-07-19 13:34:09 +00:00
|
|
|
const MTPChatAdminRights &newRights,
|
|
|
|
const QString &rank) {
|
2019-01-04 11:09:48 +00:00
|
|
|
if (mgInfo) {
|
|
|
|
// If rights are empty - still add participant? TODO check
|
|
|
|
if (!base::contains(mgInfo->lastParticipants, user)) {
|
|
|
|
mgInfo->lastParticipants.push_front(user);
|
|
|
|
setMembersCount(membersCount() + 1);
|
2019-07-18 08:51:11 +00:00
|
|
|
if (user->isBot() && !mgInfo->bots.contains(user)) {
|
2019-01-04 11:09:48 +00:00
|
|
|
mgInfo->bots.insert(user);
|
|
|
|
if (mgInfo->botStatus != 0 && mgInfo->botStatus < 2) {
|
|
|
|
mgInfo->botStatus = 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If rights are empty - still remove restrictions? TODO check
|
|
|
|
if (mgInfo->lastRestricted.contains(user)) {
|
|
|
|
mgInfo->lastRestricted.remove(user);
|
|
|
|
if (restrictedCount() > 0) {
|
|
|
|
setRestrictedCount(restrictedCount() - 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto userId = peerToUser(user->id);
|
|
|
|
auto it = mgInfo->lastAdmins.find(user);
|
2019-07-05 13:38:38 +00:00
|
|
|
if (newRights.c_chatAdminRights().vflags().v != 0) {
|
2019-01-04 11:09:48 +00:00
|
|
|
auto lastAdmin = MegagroupInfo::Admin { newRights };
|
|
|
|
lastAdmin.canEdit = true;
|
|
|
|
if (it == mgInfo->lastAdmins.cend()) {
|
|
|
|
mgInfo->lastAdmins.emplace(user, lastAdmin);
|
|
|
|
setAdminsCount(adminsCount() + 1);
|
|
|
|
} else {
|
|
|
|
it->second = lastAdmin;
|
|
|
|
}
|
2019-07-19 13:34:09 +00:00
|
|
|
Data::ChannelAdminChanges(this).add(userId, rank);
|
2019-01-04 11:09:48 +00:00
|
|
|
} else {
|
|
|
|
if (it != mgInfo->lastAdmins.cend()) {
|
|
|
|
mgInfo->lastAdmins.erase(it);
|
|
|
|
if (adminsCount() > 0) {
|
|
|
|
setAdminsCount(adminsCount() - 1);
|
|
|
|
}
|
|
|
|
}
|
2019-07-19 13:34:09 +00:00
|
|
|
Data::ChannelAdminChanges(this).remove(userId);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
if (oldRights.c_chatAdminRights().vflags().v && !newRights.c_chatAdminRights().vflags().v) {
|
2019-01-04 11:09:48 +00:00
|
|
|
// We removed an admin.
|
|
|
|
if (adminsCount() > 1) {
|
|
|
|
setAdminsCount(adminsCount() - 1);
|
|
|
|
}
|
2019-07-18 08:51:11 +00:00
|
|
|
if (!isMegagroup() && user->isBot() && membersCount() > 1) {
|
2019-01-04 11:09:48 +00:00
|
|
|
// Removing bot admin removes it from channel.
|
|
|
|
setMembersCount(membersCount() - 1);
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
} else if (!oldRights.c_chatAdminRights().vflags().v && newRights.c_chatAdminRights().vflags().v) {
|
2019-01-04 11:09:48 +00:00
|
|
|
// We added an admin.
|
|
|
|
setAdminsCount(adminsCount() + 1);
|
|
|
|
updateFullForced();
|
|
|
|
}
|
2019-01-10 06:26:08 +00:00
|
|
|
Notify::peerUpdatedDelayed(
|
|
|
|
this,
|
|
|
|
Notify::PeerUpdate::Flag::AdminsChanged);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::applyEditBanned(not_null<UserData*> user, const MTPChatBannedRights &oldRights, const MTPChatBannedRights &newRights) {
|
|
|
|
auto flags = Notify::PeerUpdate::Flag::BannedUsersChanged | Notify::PeerUpdate::Flag::None;
|
2019-07-05 13:38:38 +00:00
|
|
|
auto isKicked = (newRights.c_chatBannedRights().vflags().v & MTPDchatBannedRights::Flag::f_view_messages);
|
|
|
|
auto isRestricted = !isKicked && (newRights.c_chatBannedRights().vflags().v != 0);
|
2019-01-04 11:09:48 +00:00
|
|
|
if (mgInfo) {
|
|
|
|
// If rights are empty - still remove admin? TODO check
|
|
|
|
if (mgInfo->lastAdmins.contains(user)) {
|
|
|
|
mgInfo->lastAdmins.remove(user);
|
|
|
|
if (adminsCount() > 1) {
|
|
|
|
setAdminsCount(adminsCount() - 1);
|
|
|
|
} else {
|
|
|
|
flags |= Notify::PeerUpdate::Flag::AdminsChanged;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
auto it = mgInfo->lastRestricted.find(user);
|
|
|
|
if (isRestricted) {
|
|
|
|
if (it == mgInfo->lastRestricted.cend()) {
|
|
|
|
mgInfo->lastRestricted.emplace(user, MegagroupInfo::Restricted { newRights });
|
|
|
|
setRestrictedCount(restrictedCount() + 1);
|
|
|
|
} else {
|
|
|
|
it->second.rights = newRights;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (it != mgInfo->lastRestricted.cend()) {
|
|
|
|
mgInfo->lastRestricted.erase(it);
|
|
|
|
if (restrictedCount() > 0) {
|
|
|
|
setRestrictedCount(restrictedCount() - 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isKicked) {
|
|
|
|
auto i = ranges::find(mgInfo->lastParticipants, user);
|
|
|
|
if (i != mgInfo->lastParticipants.end()) {
|
|
|
|
mgInfo->lastParticipants.erase(i);
|
|
|
|
}
|
|
|
|
if (membersCount() > 1) {
|
|
|
|
setMembersCount(membersCount() - 1);
|
|
|
|
} else {
|
|
|
|
mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated;
|
|
|
|
mgInfo->lastParticipantsCount = 0;
|
|
|
|
}
|
|
|
|
setKickedCount(kickedCount() + 1);
|
|
|
|
if (mgInfo->bots.contains(user)) {
|
|
|
|
mgInfo->bots.remove(user);
|
|
|
|
if (mgInfo->bots.empty() && mgInfo->botStatus > 0) {
|
|
|
|
mgInfo->botStatus = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
flags |= Notify::PeerUpdate::Flag::MembersChanged;
|
|
|
|
owner().removeMegagroupParticipant(this, user);
|
|
|
|
}
|
|
|
|
}
|
2019-07-19 13:34:09 +00:00
|
|
|
Data::ChannelAdminChanges(this).remove(peerToUser(user->id));
|
2019-01-04 11:09:48 +00:00
|
|
|
} else {
|
|
|
|
if (isKicked) {
|
|
|
|
if (membersCount() > 1) {
|
|
|
|
setMembersCount(membersCount() - 1);
|
|
|
|
flags |= Notify::PeerUpdate::Flag::MembersChanged;
|
|
|
|
}
|
|
|
|
setKickedCount(kickedCount() + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Notify::peerUpdatedDelayed(this, flags);
|
|
|
|
}
|
|
|
|
|
2019-04-09 13:18:47 +00:00
|
|
|
void ChannelData::markForbidden() {
|
|
|
|
owner().processChat(MTP_channelForbidden(
|
|
|
|
MTP_flags(isMegagroup()
|
|
|
|
? MTPDchannelForbidden::Flag::f_megagroup
|
|
|
|
: MTPDchannelForbidden::Flag::f_broadcast),
|
|
|
|
MTP_int(bareId()),
|
|
|
|
MTP_long(access),
|
|
|
|
MTP_string(name),
|
|
|
|
MTPint()));
|
|
|
|
}
|
|
|
|
|
2019-01-04 11:09:48 +00:00
|
|
|
bool ChannelData::isGroupAdmin(not_null<UserData*> user) const {
|
|
|
|
if (auto info = mgInfo.get()) {
|
|
|
|
return info->admins.contains(peerToUser(user->id));
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString ChannelData::unavailableReason() const {
|
|
|
|
return _unavailableReason;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setUnavailableReason(const QString &text) {
|
|
|
|
if (_unavailableReason != text) {
|
|
|
|
_unavailableReason = text;
|
|
|
|
Notify::peerUpdatedDelayed(
|
|
|
|
this,
|
|
|
|
Notify::PeerUpdate::Flag::UnavailableReasonChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setAvailableMinId(MsgId availableMinId) {
|
|
|
|
if (_availableMinId != availableMinId) {
|
|
|
|
_availableMinId = availableMinId;
|
|
|
|
if (pinnedMessageId() <= _availableMinId) {
|
|
|
|
clearPinnedMessage();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canBanMembers() const {
|
2019-01-10 06:26:08 +00:00
|
|
|
return amCreator()
|
|
|
|
|| (adminRights() & AdminRight::f_ban_users);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canEditMessages() const {
|
2019-01-10 06:26:08 +00:00
|
|
|
return amCreator()
|
|
|
|
|| (adminRights() & AdminRight::f_edit_messages);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canDeleteMessages() const {
|
2019-01-10 06:26:08 +00:00
|
|
|
return amCreator()
|
|
|
|
|| (adminRights() & AdminRight::f_delete_messages);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::anyoneCanAddMembers() const {
|
2019-01-10 06:26:08 +00:00
|
|
|
return !(defaultRestrictions() & Restriction::f_invite_users);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::hiddenPreHistory() const {
|
|
|
|
return (fullFlags() & MTPDchannelFull::Flag::f_hidden_prehistory);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canAddMembers() const {
|
2019-01-30 10:49:36 +00:00
|
|
|
return isMegagroup()
|
|
|
|
? !amRestricted(ChatRestriction::f_invite_users)
|
|
|
|
: ((adminRights() & AdminRight::f_invite_users) || amCreator());
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
2019-01-10 06:26:08 +00:00
|
|
|
bool ChannelData::canSendPolls() const {
|
|
|
|
return canWrite() && !amRestricted(ChatRestriction::f_send_polls);
|
|
|
|
}
|
|
|
|
|
2019-01-04 11:09:48 +00:00
|
|
|
bool ChannelData::canAddAdmins() const {
|
2019-01-05 10:50:04 +00:00
|
|
|
return amCreator()
|
|
|
|
|| (adminRights() & AdminRight::f_add_admins);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canPublish() const {
|
2019-01-05 10:50:04 +00:00
|
|
|
return amCreator()
|
|
|
|
|| (adminRights() & AdminRight::f_post_messages);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canWrite() const {
|
|
|
|
// Duplicated in Data::CanWriteValue().
|
|
|
|
return amIn()
|
|
|
|
&& (canPublish()
|
|
|
|
|| (!isBroadcast()
|
2019-01-05 10:50:04 +00:00
|
|
|
&& !amRestricted(Restriction::f_send_messages)));
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canViewMembers() const {
|
|
|
|
return fullFlags()
|
|
|
|
& MTPDchannelFull::Flag::f_can_view_participants;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canViewAdmins() const {
|
|
|
|
return (isMegagroup() || hasAdminRights() || amCreator());
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canViewBanned() const {
|
|
|
|
return (hasAdminRights() || amCreator());
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canEditInformation() const {
|
2019-01-28 10:09:46 +00:00
|
|
|
return isMegagroup()
|
|
|
|
? !amRestricted(Restriction::f_change_info)
|
|
|
|
: ((adminRights() & AdminRight::f_change_info) || amCreator());
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
2019-01-07 12:55:49 +00:00
|
|
|
bool ChannelData::canEditPermissions() const {
|
2019-01-14 06:34:51 +00:00
|
|
|
return isMegagroup()
|
|
|
|
&& ((adminRights() & AdminRight::f_ban_users) || amCreator());
|
2019-01-07 12:55:49 +00:00
|
|
|
}
|
|
|
|
|
2019-01-04 11:09:48 +00:00
|
|
|
bool ChannelData::canEditSignatures() const {
|
2019-03-20 09:22:58 +00:00
|
|
|
return isChannel() && canEditInformation();
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canEditPreHistoryHidden() const {
|
2019-03-20 09:22:58 +00:00
|
|
|
return isMegagroup()
|
|
|
|
&& ((adminRights() & AdminRight::f_ban_users) || amCreator())
|
2019-01-08 13:57:22 +00:00
|
|
|
&& (!isPublic() || canEditUsername());
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canEditUsername() const {
|
|
|
|
return amCreator()
|
2019-01-05 10:50:04 +00:00
|
|
|
&& (fullFlags() & MTPDchannelFull::Flag::f_can_set_username);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canEditStickers() const {
|
2019-01-05 10:50:04 +00:00
|
|
|
return (fullFlags() & MTPDchannelFull::Flag::f_can_set_stickers);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canDelete() const {
|
|
|
|
constexpr auto kDeleteChannelMembersLimit = 1000;
|
|
|
|
return amCreator()
|
|
|
|
&& (membersCount() <= kDeleteChannelMembersLimit);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canEditLastAdmin(not_null<UserData*> user) const {
|
|
|
|
// Duplicated in ParticipantsBoxController::canEditAdmin :(
|
|
|
|
if (mgInfo) {
|
|
|
|
auto i = mgInfo->lastAdmins.find(user);
|
|
|
|
if (i != mgInfo->lastAdmins.cend()) {
|
|
|
|
return i->second.canEdit;
|
|
|
|
}
|
|
|
|
return (user != mgInfo->creator);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canEditAdmin(not_null<UserData*> user) const {
|
|
|
|
// Duplicated in ParticipantsBoxController::canEditAdmin :(
|
|
|
|
if (user->isSelf()) {
|
|
|
|
return false;
|
|
|
|
} else if (amCreator()) {
|
|
|
|
return true;
|
|
|
|
} else if (!canEditLastAdmin(user)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return adminRights() & AdminRight::f_add_admins;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChannelData::canRestrictUser(not_null<UserData*> user) const {
|
|
|
|
// Duplicated in ParticipantsBoxController::canRestrictUser :(
|
|
|
|
if (user->isSelf()) {
|
|
|
|
return false;
|
|
|
|
} else if (amCreator()) {
|
|
|
|
return true;
|
|
|
|
} else if (!canEditLastAdmin(user)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return adminRights() & AdminRight::f_ban_users;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setAdminRights(const MTPChatAdminRights &rights) {
|
2019-07-05 13:38:38 +00:00
|
|
|
if (rights.c_chatAdminRights().vflags().v == adminRights()) {
|
2019-01-04 11:09:48 +00:00
|
|
|
return;
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
_adminRights.set(rights.c_chatAdminRights().vflags().v);
|
2019-01-04 11:09:48 +00:00
|
|
|
if (isMegagroup()) {
|
|
|
|
const auto self = session().user();
|
|
|
|
if (hasAdminRights()) {
|
|
|
|
if (!amCreator()) {
|
|
|
|
auto me = MegagroupInfo::Admin { rights };
|
|
|
|
me.canEdit = false;
|
|
|
|
mgInfo->lastAdmins.emplace(self, me);
|
|
|
|
}
|
|
|
|
mgInfo->lastRestricted.remove(self);
|
|
|
|
} else {
|
|
|
|
mgInfo->lastAdmins.remove(self);
|
|
|
|
}
|
|
|
|
}
|
2019-01-05 10:50:04 +00:00
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::RightsChanged | UpdateFlag::AdminsChanged | UpdateFlag::BannedUsersChanged);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
|
|
|
|
2019-01-05 10:50:04 +00:00
|
|
|
void ChannelData::setRestrictions(const MTPChatBannedRights &rights) {
|
2019-07-05 13:38:38 +00:00
|
|
|
if (rights.c_chatBannedRights().vflags().v == restrictions()
|
|
|
|
&& rights.c_chatBannedRights().vuntil_date().v == _restrictedUntil) {
|
2019-01-04 11:09:48 +00:00
|
|
|
return;
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
_restrictedUntil = rights.c_chatBannedRights().vuntil_date().v;
|
|
|
|
_restrictions.set(rights.c_chatBannedRights().vflags().v);
|
2019-01-04 11:09:48 +00:00
|
|
|
if (isMegagroup()) {
|
|
|
|
const auto self = session().user();
|
|
|
|
if (hasRestrictions()) {
|
|
|
|
if (!amCreator()) {
|
|
|
|
auto me = MegagroupInfo::Restricted { rights };
|
|
|
|
mgInfo->lastRestricted.emplace(self, me);
|
|
|
|
}
|
|
|
|
mgInfo->lastAdmins.remove(self);
|
2019-07-19 13:34:09 +00:00
|
|
|
Data::ChannelAdminChanges(this).remove(session().userId());
|
2019-01-04 11:09:48 +00:00
|
|
|
} else {
|
|
|
|
mgInfo->lastRestricted.remove(self);
|
|
|
|
}
|
|
|
|
}
|
2019-01-05 10:50:04 +00:00
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::RightsChanged | UpdateFlag::AdminsChanged | UpdateFlag::BannedUsersChanged);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setDefaultRestrictions(const MTPChatBannedRights &rights) {
|
2019-07-05 13:38:38 +00:00
|
|
|
if (rights.c_chatBannedRights().vflags().v == defaultRestrictions()) {
|
2019-01-05 10:50:04 +00:00
|
|
|
return;
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
_defaultRestrictions.set(rights.c_chatBannedRights().vflags().v);
|
2019-01-05 10:50:04 +00:00
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::RightsChanged);
|
2019-01-04 11:09:48 +00:00
|
|
|
}
|
2019-01-13 08:03:34 +00:00
|
|
|
|
|
|
|
auto ChannelData::applyUpdateVersion(int version) -> UpdateStatus {
|
|
|
|
if (_version > version) {
|
|
|
|
return UpdateStatus::TooOld;
|
|
|
|
} else if (_version + 1 < version) {
|
|
|
|
session().api().requestPeer(this);
|
|
|
|
return UpdateStatus::Skipped;
|
|
|
|
}
|
|
|
|
setVersion(version);
|
|
|
|
return UpdateStatus::Good;
|
|
|
|
}
|
|
|
|
|
2019-01-14 06:34:51 +00:00
|
|
|
ChatData *ChannelData::getMigrateFromChat() const {
|
|
|
|
if (const auto info = mgInfo.get()) {
|
|
|
|
return info->getMigrateFromChat();
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setMigrateFromChat(ChatData *chat) {
|
|
|
|
Expects(mgInfo != nullptr);
|
|
|
|
|
|
|
|
const auto info = mgInfo.get();
|
|
|
|
if (chat != info->getMigrateFromChat()) {
|
|
|
|
info->setMigrateFromChat(chat);
|
|
|
|
if (amIn()) {
|
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::MigrationChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-16 11:46:50 +00:00
|
|
|
int ChannelData::slowmodeSeconds() const {
|
|
|
|
return _slowmodeSeconds;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChannelData::setSlowmodeSeconds(int seconds) {
|
|
|
|
if (_slowmodeSeconds == seconds) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_slowmodeSeconds = seconds;
|
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::ChannelSlowmode);
|
|
|
|
}
|
|
|
|
|
|
|
|
TimeId ChannelData::slowmodeLastMessage() const {
|
|
|
|
return (hasAdminRights() || amCreator()) ? 0 : _slowmodeLastMessage;
|
|
|
|
}
|
|
|
|
|
2019-07-16 13:59:50 +00:00
|
|
|
void ChannelData::growSlowmodeLastMessage(TimeId when) {
|
2019-08-20 09:42:13 +00:00
|
|
|
const auto now = base::unixtime::now();
|
|
|
|
accumulate_min(when, now);
|
|
|
|
if (_slowmodeLastMessage > now) {
|
|
|
|
_slowmodeLastMessage = when;
|
|
|
|
} else if (_slowmodeLastMessage >= when) {
|
2019-07-16 11:46:50 +00:00
|
|
|
return;
|
2019-08-20 09:42:13 +00:00
|
|
|
} else {
|
|
|
|
_slowmodeLastMessage = when;
|
2019-07-16 11:46:50 +00:00
|
|
|
}
|
|
|
|
Notify::peerUpdatedDelayed(this, UpdateFlag::ChannelSlowmode);
|
|
|
|
}
|
|
|
|
|
2019-01-13 08:03:34 +00:00
|
|
|
namespace Data {
|
|
|
|
|
2019-01-14 06:34:51 +00:00
|
|
|
void ApplyMigration(
|
|
|
|
not_null<ChatData*> chat,
|
|
|
|
not_null<ChannelData*> channel) {
|
|
|
|
Expects(channel->isMegagroup());
|
|
|
|
|
|
|
|
chat->setMigrateToChannel(channel);
|
|
|
|
channel->setMigrateFromChat(chat);
|
|
|
|
}
|
|
|
|
|
2019-01-13 08:03:34 +00:00
|
|
|
void ApplyChannelUpdate(
|
|
|
|
not_null<ChannelData*> channel,
|
|
|
|
const MTPDupdateChatDefaultBannedRights &update) {
|
2019-07-05 13:38:38 +00:00
|
|
|
if (channel->applyUpdateVersion(update.vversion().v)
|
2019-01-13 08:03:34 +00:00
|
|
|
!= ChannelData::UpdateStatus::Good) {
|
|
|
|
return;
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
channel->setDefaultRestrictions(update.vdefault_banned_rights());
|
2019-01-13 08:03:34 +00:00
|
|
|
}
|
|
|
|
|
2019-04-23 09:40:14 +00:00
|
|
|
void ApplyChannelUpdate(
|
|
|
|
not_null<ChannelData*> channel,
|
|
|
|
const MTPDchannelFull &update) {
|
2019-07-05 13:38:38 +00:00
|
|
|
channel->setAvailableMinId(update.vavailable_min_id().value_or_empty());
|
2019-04-23 09:40:14 +00:00
|
|
|
auto canViewAdmins = channel->canViewAdmins();
|
|
|
|
auto canViewMembers = channel->canViewMembers();
|
|
|
|
auto canEditStickers = channel->canEditStickers();
|
|
|
|
|
2019-07-05 13:38:38 +00:00
|
|
|
channel->setFullFlags(update.vflags().v);
|
|
|
|
channel->setUserpicPhoto(update.vchat_photo());
|
|
|
|
if (const auto migratedFrom = update.vmigrated_from_chat_id()) {
|
2019-04-23 09:40:14 +00:00
|
|
|
channel->addFlags(MTPDchannel::Flag::f_megagroup);
|
2019-07-05 13:38:38 +00:00
|
|
|
const auto chat = channel->owner().chat(migratedFrom->v);
|
2019-04-23 09:40:14 +00:00
|
|
|
Data::ApplyMigration(chat, channel);
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
for (const auto &item : update.vbot_info().v) {
|
2019-04-23 09:40:14 +00:00
|
|
|
auto &owner = channel->owner();
|
|
|
|
item.match([&](const MTPDbotInfo &info) {
|
2019-07-05 13:38:38 +00:00
|
|
|
if (const auto user = owner.userLoaded(info.vuser_id().v)) {
|
2019-04-23 09:40:14 +00:00
|
|
|
user->setBotInfo(item);
|
|
|
|
channel->session().api().fullPeerUpdated().notify(user);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
channel->setAbout(qs(update.vabout()));
|
|
|
|
channel->setMembersCount(update.vparticipants_count().value_or_empty());
|
|
|
|
channel->setAdminsCount(update.vadmins_count().value_or_empty());
|
|
|
|
channel->setRestrictedCount(update.vbanned_count().value_or_empty());
|
|
|
|
channel->setKickedCount(update.vkicked_count().value_or_empty());
|
2019-07-16 11:46:50 +00:00
|
|
|
channel->setSlowmodeSeconds(update.vslowmode_seconds().value_or_empty());
|
|
|
|
if (const auto next = update.vslowmode_next_send_date()) {
|
2019-08-20 09:42:13 +00:00
|
|
|
channel->growSlowmodeLastMessage(
|
|
|
|
next->v - channel->slowmodeSeconds());
|
2019-07-16 11:46:50 +00:00
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
channel->setInviteLink(update.vexported_invite().match([&](
|
2019-06-21 12:27:46 +00:00
|
|
|
const MTPDchatInviteExported &data) {
|
2019-07-05 13:38:38 +00:00
|
|
|
return qs(data.vlink());
|
2019-04-23 09:40:14 +00:00
|
|
|
}, [&](const MTPDchatInviteEmpty &) {
|
|
|
|
return QString();
|
|
|
|
}));
|
2019-07-05 13:38:38 +00:00
|
|
|
if (const auto location = update.vlocation()) {
|
|
|
|
channel->setLocation(*location);
|
|
|
|
} else {
|
|
|
|
channel->setLocation(MTP_channelLocationEmpty());
|
|
|
|
}
|
|
|
|
if (const auto chat = update.vlinked_chat_id()) {
|
|
|
|
channel->setLinkedChat(channel->owner().channelLoaded(chat->v));
|
|
|
|
} else {
|
|
|
|
channel->setLinkedChat(nullptr);
|
|
|
|
}
|
2019-04-23 09:40:14 +00:00
|
|
|
if (const auto history = channel->owner().historyLoaded(channel)) {
|
2019-07-05 13:38:38 +00:00
|
|
|
if (const auto available = update.vavailable_min_id()) {
|
|
|
|
history->clearUpTill(available->v);
|
|
|
|
}
|
|
|
|
const auto folderId = update.vfolder_id().value_or_empty();
|
2019-04-23 09:40:14 +00:00
|
|
|
const auto folder = folderId
|
|
|
|
? channel->owner().folderLoaded(folderId)
|
|
|
|
: nullptr;
|
|
|
|
if (folder && history->folder() != folder) {
|
|
|
|
// If history folder is unknown or not synced, request both.
|
|
|
|
channel->session().api().requestDialogEntry(history);
|
|
|
|
channel->session().api().requestDialogEntry(folder);
|
|
|
|
} else if (!history->folderKnown()
|
2019-07-05 13:38:38 +00:00
|
|
|
|| channel->pts() != update.vpts().v) {
|
2019-04-23 09:40:14 +00:00
|
|
|
channel->session().api().requestDialogEntry(history);
|
|
|
|
} else {
|
|
|
|
history->applyDialogFields(
|
|
|
|
history->folder(),
|
2019-07-05 13:38:38 +00:00
|
|
|
update.vunread_count().v,
|
|
|
|
update.vread_inbox_max_id().v,
|
|
|
|
update.vread_outbox_max_id().v);
|
2019-04-23 09:40:14 +00:00
|
|
|
}
|
|
|
|
}
|
2019-07-05 13:38:38 +00:00
|
|
|
if (const auto pinned = update.vpinned_msg_id()) {
|
|
|
|
channel->setPinnedMessageId(pinned->v);
|
2019-04-23 09:40:14 +00:00
|
|
|
} else {
|
|
|
|
channel->clearPinnedMessage();
|
|
|
|
}
|
|
|
|
if (channel->isMegagroup()) {
|
2019-07-05 13:38:38 +00:00
|
|
|
const auto stickerSet = update.vstickerset();
|
|
|
|
const auto set = stickerSet ? &stickerSet->c_stickerSet() : nullptr;
|
|
|
|
const auto newSetId = (set ? set->vid().v : 0);
|
2019-04-23 09:40:14 +00:00
|
|
|
const auto oldSetId = (channel->mgInfo->stickerSet.type() == mtpc_inputStickerSetID)
|
2019-07-05 13:38:38 +00:00
|
|
|
? channel->mgInfo->stickerSet.c_inputStickerSetID().vid().v
|
2019-04-23 09:40:14 +00:00
|
|
|
: 0;
|
|
|
|
const auto stickersChanged = (canEditStickers != channel->canEditStickers())
|
|
|
|
|| (oldSetId != newSetId);
|
|
|
|
if (oldSetId != newSetId) {
|
2019-07-05 13:38:38 +00:00
|
|
|
channel->mgInfo->stickerSet = set
|
|
|
|
? MTP_inputStickerSetID(set->vid(), set->vaccess_hash())
|
2019-04-23 09:40:14 +00:00
|
|
|
: MTP_inputStickerSetEmpty();
|
|
|
|
}
|
|
|
|
if (stickersChanged) {
|
|
|
|
Notify::peerUpdatedDelayed(
|
|
|
|
channel,
|
|
|
|
Notify::PeerUpdate::Flag::ChannelStickersChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
channel->fullUpdated();
|
|
|
|
|
|
|
|
if (canViewAdmins != channel->canViewAdmins()
|
|
|
|
|| canViewMembers != channel->canViewMembers()) {
|
|
|
|
Notify::peerUpdatedDelayed(
|
|
|
|
channel,
|
|
|
|
Notify::PeerUpdate::Flag::RightsChanged);
|
|
|
|
}
|
|
|
|
|
|
|
|
channel->session().api().applyNotifySettings(
|
|
|
|
MTP_inputNotifyPeer(channel->input),
|
2019-07-05 13:38:38 +00:00
|
|
|
update.vnotify_settings());
|
2019-04-23 09:40:14 +00:00
|
|
|
}
|
|
|
|
|
2019-07-24 06:37:10 +00:00
|
|
|
void ApplyMegagroupAdmins(
|
2019-07-19 13:34:09 +00:00
|
|
|
not_null<ChannelData*> channel,
|
|
|
|
const MTPDchannels_channelParticipants &data) {
|
2019-07-24 06:37:10 +00:00
|
|
|
Expects(channel->isMegagroup());
|
|
|
|
|
2019-07-19 13:34:09 +00:00
|
|
|
channel->owner().processUsers(data.vusers());
|
|
|
|
|
|
|
|
const auto &list = data.vparticipants().v;
|
2019-07-24 06:37:10 +00:00
|
|
|
const auto i = ranges::find(
|
|
|
|
list,
|
|
|
|
mtpc_channelParticipantCreator,
|
|
|
|
&MTPChannelParticipant::type);
|
|
|
|
if (i != list.end()) {
|
|
|
|
const auto &data = i->c_channelParticipantCreator();
|
|
|
|
const auto userId = data.vuser_id().v;
|
|
|
|
channel->mgInfo->creator = channel->owner().userLoaded(userId);
|
|
|
|
channel->mgInfo->creatorRank = qs(data.vrank().value_or_empty());
|
|
|
|
} else {
|
|
|
|
channel->mgInfo->creator = nullptr;
|
|
|
|
channel->mgInfo->creatorRank = QString();
|
|
|
|
}
|
2019-07-19 13:34:09 +00:00
|
|
|
|
|
|
|
auto adding = base::flat_map<UserId, QString>();
|
2019-10-02 09:54:29 +00:00
|
|
|
auto admins = ranges::make_subrange(
|
2019-07-19 13:34:09 +00:00
|
|
|
list.begin(), list.end()
|
|
|
|
) | ranges::view::transform([](const MTPChannelParticipant &p) {
|
|
|
|
const auto userId = p.match([](const auto &data) {
|
|
|
|
return data.vuser_id().v;
|
|
|
|
});
|
|
|
|
const auto rank = p.match([](const MTPDchannelParticipantAdmin &data) {
|
|
|
|
return qs(data.vrank().value_or_empty());
|
|
|
|
}, [](const MTPDchannelParticipantCreator &data) {
|
|
|
|
return qs(data.vrank().value_or_empty());
|
|
|
|
}, [](const auto &data) {
|
|
|
|
return QString();
|
|
|
|
});
|
|
|
|
return std::make_pair(userId, rank);
|
|
|
|
});
|
|
|
|
for (const auto &[userId, rank] : admins) {
|
|
|
|
adding.emplace(userId, rank);
|
|
|
|
}
|
|
|
|
if (channel->mgInfo->creator) {
|
|
|
|
adding.emplace(
|
|
|
|
peerToUser(channel->mgInfo->creator->id),
|
|
|
|
channel->mgInfo->creatorRank);
|
|
|
|
}
|
|
|
|
auto removing = channel->mgInfo->admins;
|
|
|
|
if (removing.empty() && adding.empty()) {
|
|
|
|
// Add some admin-placeholder so we don't DDOS
|
|
|
|
// server with admins list requests.
|
|
|
|
LOG(("API Error: Got empty admins list from server."));
|
|
|
|
adding.emplace(0, QString());
|
|
|
|
}
|
|
|
|
|
|
|
|
Data::ChannelAdminChanges changes(channel);
|
|
|
|
for (const auto &[addingId, rank] : adding) {
|
|
|
|
if (!removing.remove(addingId)) {
|
|
|
|
changes.add(addingId, rank);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const auto &[removingId, rank] : removing) {
|
|
|
|
changes.remove(removingId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-13 08:03:34 +00:00
|
|
|
} // namespace Data
|