- This information is retrieved from the notifications endpoint.
- Add icons for the different pre defined status.
- Make functions available to QML to decide which status icon to display.
- Display the user status icon on the avatar and
move the online/offline connection status to the folder icon.
Signed-off-by: Camila <hello@camila.codes>
, _waitingForNewCredentials(false)
, _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
, _remoteWipe(new RemoteWipe(_account))
- , _userStatus(new UserStatus(this, this))
+ , _userStatus(new UserStatus(this))
+ , _notificationStatus("online")
{
qRegisterMetaType<AccountState *>("AccountState*");
emit stateChanged(_state);
}
-QString AccountState::currentUserStatus() const
+QString AccountState::status() const
{
- return _userStatus->currentUserStatus();
+ return _userStatus->status();
+}
+
+QString AccountState::statusMessage() const
+{
+ return _userStatus->message();
+}
+
+QUrl AccountState::statusIcon() const
+{
+ return _userStatus->icon();
}
QString AccountState::stateString(State state)
_navigationAppsEtagResponseHeader = value;
}
+QString AccountState::notificationStatus() const
+{
+ return _notificationStatus;
+}
+
+void AccountState::setNotificationStatus(const QString &status)
+{
+ _notificationStatus = status;
+}
+
void AccountState::checkConnectivity()
{
if (isSignedOut() || _waitingForNewCredentials) {
job->getNavigationApps();
}
-void AccountState::fetchCurrentUserStatus() {
- _userStatus->fetchCurrentUserStatus();
+void AccountState::fetchUserStatus()
+{
+ connect(_userStatus, &UserStatus::fetchUserStatusFinished, this, &AccountState::userStatusChanged);
+ _userStatus->fetchUserStatus(_account);
}
void AccountState::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){
///Asks for user credentials
void handleInvalidCredentials();
- QString currentUserStatus() const;
+ /** Returns the user status (online, dnd, away, offline, invisible)
+ * https://gist.github.com/georgehrke/55a0412007f13be1551d1f9436a39675
+ */
+ QString status() const;
+
+ /** Returns the user status Message (emoji + text)
+ */
+ QString statusMessage() const;
+
+ /** Returns the user status icon url
+ */
+ QUrl statusIcon() const;
- void fetchCurrentUserStatus();
+ /** Returns the user status retrieved by the notificatons endpoint: dnd or online
+ * https://github.com/nextcloud/desktop/issues/2318#issuecomment-680698429
+ */
+ QString notificationStatus() const;
+
+ /** Set new user status retrieved by the notificatons endpoint: dnd or online
+ */
+ void setNotificationStatus(const QString &status);
+
+ /** Fetch the user status (status, icon, message)
+ */
+ void fetchUserStatus();
public slots:
/// Triggers a ping to the server to update state and
AccountAppList _apps;
UserStatus *_userStatus;
+ QString _notificationStatus;
};
class AccountApp : public QObject
this, &ServerNotificationHandler::slotNotificationsReceived);
QObject::connect(_notificationJob.data(), &JsonApiJob::etagResponseHeaderReceived,
this, &ServerNotificationHandler::slotEtagResponseHeaderReceived);
+ QObject::connect(_notificationJob.data(), &JsonApiJob::desktopNotificationStatusReceived,
+ this, &ServerNotificationHandler::slotDesktopNotificationStatusReceived);
_notificationJob->setProperty(propertyAccountStateC, QVariant::fromValue<AccountState *>(_accountState));
_notificationJob->addRawHeader("If-None-Match", _accountState->notificationsEtagResponseHeader());
_notificationJob->start();
}
}
+void ServerNotificationHandler::slotDesktopNotificationStatusReceived(const bool status)
+{
+ auto *account = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
+ if (account != nullptr) {
+ account->setDesktopNotificationsStatus(status);
+ }
+}
+
void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
{
iconCache.insert(sender()->property("activityId").toInt(),iconData);
void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
void slotIconDownloaded(QByteArray iconData);
+ void slotDesktopNotificationStatusReceived(const bool status);
private:
QPointer<JsonApiJob> _notificationJob;
anchors.fill: parent\r
hoverEnabled: true\r
onContainsMouseChanged: {\r
- accountStateIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white")\r
+ accountStatusIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white")\r
}\r
onClicked: {\r
if (!isCurrentUser) {\r
Layout.preferredHeight: (userLineLayout.height -16)\r
Layout.preferredWidth: (userLineLayout.height -16)\r
Rectangle {\r
- id: accountStateIndicatorBackground\r
- width: accountStateIndicator.sourceSize.width + 2\r
+ id: accountStatusIndicatorBackground\r
+ width: accountStatusIndicator.sourceSize.width + 2\r
height: width\r
anchors.bottom: accountAvatar.bottom\r
anchors.right: accountAvatar.right\r
radius: width*0.5\r
}\r
Image {\r
- id: accountStateIndicator\r
- source: model.isConnected\r
- ? Style.stateOnlineImageSource\r
- : Style.stateOfflineImageSource\r
+ id: accountStatusIndicator\r
+ source: model.statusIcon\r
cache: false\r
- x: accountStateIndicatorBackground.x + 1\r
- y: accountStateIndicatorBackground.y + 1\r
+ x: accountStatusIndicatorBackground.x + 1\r
+ y: accountStatusIndicatorBackground.y + 1\r
sourceSize.width: Style.accountAvatarStateIndicatorSize\r
sourceSize.height: Style.accountAvatarStateIndicatorSize\r
\r
Accessible.role: Accessible.Indicator\r
- Accessible.name: model.isConnected ? qsTr("Account connected") : qsTr("Account not connected")\r
+ Accessible.name: model.isStatusOnline ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb")\r
}\r
}\r
\r
font.bold: true\r
}\r
Label {\r
- id: userStatus\r
+ id: userStatusMessage\r
width: 128\r
- text: status\r
+ text: statusMessage\r
elide: Text.ElideRight\r
color: "black"\r
font.pixelSize: 10\r
}\r
}\r
}\r
-\r
- Connections {\r
- target: UserModel\r
- onRefreshCurrentUserGui: {\r
- accountStateIndicator.source = model.isConnected\r
- ? Style.stateOnlineImageSource\r
- : Style.stateOfflineImageSource\r
- }\r
- }\r
} // MenuItem userLine\r
// Assemble a tray notification for the NEW notification
ConfigFile cfg;
- if (cfg.optionalServerNotifications() /*and header is not X-Nextcloud-User-Status*/) {
+ if (cfg.optionalServerNotifications() && isDesktopNotificationsAllowed()) {
if (AccountManager::instance()->accounts().count() == 1) {
emit guiLog(activity._subject, "");
} else {
slotRefreshActivities();
}
slotRefreshNotifications();
- _account.data()->fetchCurrentUserStatus();
+ _account.data()->fetchUserStatus();
timer.start();
}
}
return serverUrl;
}
-QString User::currentUserStatus() const
+QString User::status() const
{
- return _account->currentUserStatus();
+ return _account->status();
+}
+
+QString User::statusMessage() const
+{
+ return _account->statusMessage();
+}
+
+QUrl User::statusIcon() const
+{
+ return _account->statusIcon();
}
QImage User::avatar() const
return (_account->connectionStatus() == AccountState::ConnectionStatus::Connected);
}
+
+bool User::isDesktopNotificationsAllowed() const
+{
+ return _account.data()->notificationStatus() == "online";
+}
+
void User::removeAccount() const
{
AccountManager::instance()->deleteAccount(_account.data());
return _users[id]->isConnected();
}
+Q_INVOKABLE QUrl UserModel::statusIcon(const int &id)
+{
+ if (id < 0 || id >= _users.size()) {
+ return {};
+ }
+
+ return _users[id]->statusIcon();
+}
+
+
QImage UserModel::avatarById(const int &id)
{
if (id < 0 || id >= _users.size())
});
connect(u, &User::userStatusChanged, this, [this, row] {
- emit dataChanged(index(row, 0), index(row, 0), {UserModel::StatusRole});
+ emit dataChanged(index(row, 0), index(row, 0), {UserModel::StatusIconRole,
+ UserModel::StatusMessageRole});
});
_users << u;
return _users[index.row()]->name();
} else if (role == ServerRole) {
return _users[index.row()]->server();
- } else if (role == StatusRole) {
- return _users[index.row()]->currentUserStatus();
+ } else if (role == StatusIconRole) {
+ return _users[index.row()]->statusIcon();
+ } else if (role == StatusMessageRole) {
+ return _users[index.row()]->statusMessage();
} else if (role == AvatarRole) {
return _users[index.row()]->avatarUrl();
} else if (role == IsCurrentUserRole) {
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[ServerRole] = "server";
- roles[StatusRole] = "status";
+ roles[StatusIconRole] = "statusIcon";
+ roles[StatusMessageRole] = "statusMessage";
roles[AvatarRole] = "avatar";
roles[IsCurrentUserRole] = "isCurrentUser";
roles[IsConnectedRole] = "isConnected";
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString server READ server CONSTANT)
- Q_PROPERTY(QString status READ currentUserStatus NOTIFY userStatusChanged)
+ Q_PROPERTY(QUrl statusIcon READ statusIcon NOTIFY userStatusChanged)
+ Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY userStatusChanged)
Q_PROPERTY(bool hasLocalFolder READ hasLocalFolder NOTIFY hasLocalFolderChanged)
Q_PROPERTY(bool serverHasTalk READ serverHasTalk NOTIFY serverHasTalkChanged)
Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
void openLocalFolder();
QString name() const;
QString server(bool shortened = true) const;
- QString currentUserStatus() const;
bool hasLocalFolder() const;
bool serverHasTalk() const;
AccountApp *talkApp() const;
void logout() const;
void removeAccount() const;
QString avatarUrl() const;
+ bool isDesktopNotificationsAllowed() const;
+ QString status() const;
+ QString statusMessage() const;
+ QUrl statusIcon() const;
signals:
void guiLog(const QString &, const QString &);
Q_INVOKABLE bool currentUserHasLocalFolder();
int currentUserId() const;
Q_INVOKABLE bool isUserConnected(const int &id);
+ Q_INVOKABLE QUrl statusIcon(const int &id);
Q_INVOKABLE void switchCurrentUser(const int &id);
Q_INVOKABLE void login(const int &id);
Q_INVOKABLE void logout(const int &id);
enum UserRoles {
NameRole = Qt::UserRole + 1,
ServerRole,
- StatusRole,
+ StatusIconRole,
+ StatusMessageRole,
AvatarRole,
IsCurrentUserRole,
IsConnectedRole,
}\r
\r
onVisibleChanged: {\r
- currentAccountStateIndicator.source = ""\r
- currentAccountStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId)\r
+ folderStateIndicator.source = ""\r
+ folderStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId)\r
? Style.stateOnlineImageSource\r
: Style.stateOfflineImageSource\r
\r
Connections {\r
target: UserModel\r
onRefreshCurrentUserGui: {\r
- currentAccountStateIndicator.source = ""\r
- currentAccountStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId)\r
+ folderStateIndicator.source = ""\r
+ folderStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId)\r
? Style.stateOnlineImageSource\r
: Style.stateOfflineImageSource\r
}\r
Accessible.name: qsTr("Current user avatar")\r
\r
Rectangle {\r
- id: currentAccountStateIndicatorBackground\r
+ id: currentAccountStatusIndicatorBackground\r
width: Style.accountAvatarStateIndicatorSize + 2\r
height: width\r
anchors.bottom: currentAccountAvatar.bottom\r
}\r
\r
Image {\r
- id: currentAccountStateIndicator\r
- source: UserModel.isUserConnected(UserModel.currentUserId)\r
- ? Style.stateOnlineImageSource\r
- : Style.stateOfflineImageSource\r
+ id: currentAccountStatusIndicator\r
+ source: UserModel.currentUser.statusIcon\r
cache: false\r
- x: currentAccountStateIndicatorBackground.x + 1\r
- y: currentAccountStateIndicatorBackground.y + 1\r
+ x: currentAccountStatusIndicatorBackground.x + 1\r
+ y: currentAccountStatusIndicatorBackground.y + 1\r
sourceSize.width: Style.accountAvatarStateIndicatorSize\r
sourceSize.height: Style.accountAvatarStateIndicatorSize\r
\r
Accessible.role: Accessible.Indicator\r
- Accessible.name: UserModel.isUserConnected(UserModel.currentUserId()) ? qsTr("Connected") : qsTr("Disconnected")\r
+ Accessible.name: UserModel.isUserStatusOnline(UserModel.currentUserId()) ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb")\r
}\r
}\r
\r
Label {\r
id: currentUserStatus\r
width: Style.currentAccountLabelWidth\r
- text: UserModel.currentUser.status\r
+ text: UserModel.currentUser.statusMessage\r
elide: Text.ElideRight\r
color: Style.ncTextColor\r
font.pixelSize: Style.subLinePixelSize\r
Accessible.onPressAction: openLocalFolderButton.clicked()\r
}\r
\r
+ Rectangle {\r
+ id: folderStateIndicatorBackground\r
+ width: Style.folderStateIndicatorSize\r
+ height: width\r
+ anchors.top: openLocalFolderButton.verticalCenter\r
+ anchors.left: openLocalFolderButton.horizontalCenter\r
+ color: Style.ncBlue\r
+ radius: width*0.5\r
+ }\r
+\r
+ Rectangle {\r
+ id: folderStateRectangle\r
+ width: Style.folderStateIndicatorSize\r
+ height: width\r
+ anchors.bottom: openLocalFolderButton.bottom\r
+ anchors.right: openLocalFolderButton.right\r
+ color: openLocalFolderButton.containsMouse ? "white" : "transparent"\r
+ opacity: 0.2\r
+ radius: width*0.5\r
+ }\r
+\r
+ Image {\r
+ id: folderStateIndicator\r
+ source: UserModel.isUserConnected(UserModel.currentUserId)\r
+ ? Style.stateOnlineImageSource\r
+ : Style.stateOfflineImageSource\r
+ cache: false\r
+ x: folderStateIndicatorBackground.x\r
+ y: folderStateIndicatorBackground.y\r
+ sourceSize.width: Style.folderStateIndicatorSize\r
+ sourceSize.height: Style.folderStateIndicatorSize\r
+\r
+ Accessible.role: Accessible.Indicator\r
+ Accessible.name: UserModel.isUserConnected(UserModel.currentUserId()) ? qsTr("Connected") : qsTr("Disconnected")\r
+ }\r
+\r
HeaderButton {\r
id: trayWindowTalkButton\r
\r
#include "networkjobs.h"
#include "folderman.h"
#include "creds/abstractcredentials.h"
-#include <theme.h>
+#include "theme.h"
#include <QTimer>
#include <QJsonDocument>
namespace OCC {
-UserStatus::UserStatus(AccountState *accountState, QObject *parent)
+UserStatus::UserStatus(QObject *parent)
: QObject(parent)
- , _accountState(accountState)
+ , _status("online")
+ , _message("")
{
- connect(this, &UserStatus::fetchedCurrentUserStatus, _accountState, &AccountState::userStatusChanged);
+
}
-void UserStatus::fetchCurrentUserStatus()
+void UserStatus::fetchUserStatus(AccountPtr account)
{
if (_job) {
_job->deleteLater();
}
- AccountPtr account = _accountState->account();
_job = new JsonApiJob(account, QStringLiteral("/ocs/v2.php/apps/user_status/api/v1/user_status"), this);
- connect(_job.data(), &JsonApiJob::jsonReceived, this, &UserStatus::slotFetchedCurrentStatus);
+ connect(_job.data(), &JsonApiJob::jsonReceived, this, &UserStatus::slotFetchUserStatusFinished);
_job->start();
}
-void UserStatus::slotFetchedCurrentStatus(const QJsonDocument &json)
+void UserStatus::slotFetchUserStatusFinished(const QJsonDocument &json)
{
const auto retrievedData = json.object().value("ocs").toObject().value("data").toObject();
- const auto icon = retrievedData.value("icon").toString();
+ const auto emoji = retrievedData.value("icon").toString();
const auto message = retrievedData.value("message").toString();
- auto status = retrievedData.value("status").toString();
+ _status = retrievedData.value("status").toString();
- if(message.isEmpty()) {
- if(status == "dnd") {
- status = tr("Do not disturb");
- }
- } else {
- status = message;
- }
+ const auto visibleStatusText = message.isEmpty()
+ ? _status == "dnd"? tr("Do not disturb") : _status
+ : message;
+
+ _message = QString("%1 %2").arg(emoji, visibleStatusText);
+ emit fetchUserStatusFinished();
+}
+
+QString UserStatus::status() const
+{
+ return _status;
+}
- _currentUserStatus = QString("%1 %2").arg(icon, status);
- emit fetchedCurrentUserStatus();
+QString UserStatus::message() const
+{
+ return _message;
}
-QString UserStatus::currentUserStatus() const
+QUrl UserStatus::icon() const
{
- return _currentUserStatus;
+ // online, away, dnd, invisible, offline
+ if(_status == "online") {
+ return Theme::instance()->statusOnlineImageSource();
+ } else if (_status == "away") {
+ return Theme::instance()->statusAwayImageSource();
+ } else if (_status == "dnd") {
+ return Theme::instance()->statusDoNotDisturbImageSource();
+ } else if (_status == "invisible") {
+ return Theme::instance()->statusInvisibleImageSource();
+ } else if (_status == "offline") {
+ return Theme::instance()->statusInvisibleImageSource();
+ }
+
+ return Theme::instance()->statusOnlineImageSource();
}
} // namespace OCC
#include <QObject>
#include <QPointer>
-#include <QVariant>
+#include "accountfwd.h"
namespace OCC {
-class AccountState;
class JsonApiJob;
class UserStatus : public QObject
Q_OBJECT
public:
- explicit UserStatus(AccountState *accountState, QObject *parent = nullptr);
- void fetchCurrentUserStatus();
- QString currentUserStatus() const;
+ explicit UserStatus(QObject *parent = nullptr);
+ void fetchUserStatus(AccountPtr account);
+ QString status() const;
+ QString message() const;
+ QUrl icon() const;
private slots:
- void slotFetchedCurrentStatus(const QJsonDocument &json);
+ void slotFetchUserStatusFinished(const QJsonDocument &json);
signals:
- void fetchedCurrentUserStatus();
+ void fetchUserStatusFinished();
private:
- QPointer<AccountState> _accountState;
QPointer<JsonApiJob> _job; // the currently running job
- QString _currentUserStatus;
+ QString _status;
+ QString _message;
};
if(reply()->rawHeaderList().contains("ETag"))
emit etagResponseHeaderReceived(reply()->rawHeader("ETag"), statusCode);
+ const auto desktopNotificationStatus = reply()->rawHeader(QByteArray("X-Nextcloud-User-Status"));
+ if(!desktopNotificationStatus.isEmpty()) {
+ emit desktopNotificationStatusReceived(desktopNotificationStatus == "online");
+ }
+
QJsonParseError error;
auto json = QJsonDocument::fromJson(jsonStr.toUtf8(), &error);
// empty or invalid response and status code is != 304 because jsonStr is expected to be empty
* @param statusCode - the OCS status code: 100 (!) for success
*/
void etagResponseHeaderReceived(const QByteArray &value, int statusCode);
+
+ /**
+ * @brief desktopNotificationStatusReceived - signal to report the if user is online or dnd
+ * @param status - set desktop notifications allowed status
+ */
+ void desktopNotificationStatusReceived(const bool status);
private:
QUrlQuery _additionalParams;
return imagePathToUrl(themeImagePath("state-offline", 16));
}
+QUrl Theme::statusOnlineImageSource() const
+{
+ return imagePathToUrl(themeImagePath("user-status-online", 16));
+}
+
+QUrl Theme::statusDoNotDisturbImageSource() const
+{
+ return imagePathToUrl(themeImagePath("user-status-dnd", 16));
+}
+
+QUrl Theme::statusAwayImageSource() const
+{
+ return imagePathToUrl(themeImagePath("user-status-away", 16));
+}
+
+QUrl Theme::statusInvisibleImageSource() const
+{
+ return imagePathToUrl(themeImagePath("user-status-invisible", 16));
+}
+
QString Theme::version() const
{
return MIRALL_VERSION_STRING;
Q_PROPERTY(QString appName READ appName CONSTANT)
Q_PROPERTY(QUrl stateOnlineImageSource READ stateOnlineImageSource CONSTANT)
Q_PROPERTY(QUrl stateOfflineImageSource READ stateOfflineImageSource CONSTANT)
+ Q_PROPERTY(QUrl stateOnlineImageSource READ stateOnlineImageSource CONSTANT)
+ Q_PROPERTY(QUrl statusOnlineImageSource READ statusOnlineImageSource CONSTANT)
+ Q_PROPERTY(QUrl statusDoNotDisturbImageSource READ statusDoNotDisturbImageSource CONSTANT)
+ Q_PROPERTY(QUrl statusAwayImageSource READ statusAwayImageSource CONSTANT)
+ Q_PROPERTY(QUrl statusInvisibleImageSource READ statusInvisibleImageSource CONSTANT)
#ifndef TOKEN_AUTH_ONLY
Q_PROPERTY(QIcon folderDisabledIcon READ folderDisabledIcon CONSTANT)
Q_PROPERTY(QIcon folderOfflineIcon READ folderOfflineIcon CONSTANT)
* @return QUrl full path to an icon
*/
QUrl stateOfflineImageSource() const;
+
+ /**
+ * @brief Returns full path to an online user status icon
+ * @return QUrl full path to an icon
+ */
+ QUrl statusOnlineImageSource() const;
+
+ /**
+ * @brief Returns full path to an do not disturb user status icon
+ * @return QUrl full path to an icon
+ */
+ QUrl statusDoNotDisturbImageSource() const;
+
+ /**
+ * @brief Returns full path to an away user status icon
+ * @return QUrl full path to an icon
+ */
+ QUrl statusAwayImageSource() const;
+
+ /**
+ * @brief Returns full path to an invisible user status icon
+ * @return QUrl full path to an icon
+ */
+ QUrl statusInvisibleImageSource() const;
/**
* @brief configFileName
<file>theme/share.svg</file>
<file>theme/reply.svg</file>
<file>theme/magnifying-glass.svg</file>
+ <file>theme/colored/user-status-online.svg</file>
+ <file>theme/colored/user-status-invisible.svg</file>
+ <file>theme/colored/user-status-away.svg</file>
+ <file>theme/colored/user-status-dnd.svg</file>
</qresource>
</RCC>
\r
property int accountAvatarSize: (trayWindowHeaderHeight - 16)\r
property int accountAvatarStateIndicatorSize: 16\r
+ property int folderStateIndicatorSize: 16\r
property int accountLabelWidth: 128\r
\r
property int accountDropDownCaretSize: 20\r
--- /dev/null
+<svg width="24" height="24" enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect width="24" height="24" fill="none"/><path d="m10.615 2.1094c-4.8491 0.68106-8.6152 4.8615-8.6152 9.8906 0 5.5 4.5 10 10 10 5.0292 0 9.2096-3.7661 9.8906-8.6152-1.4654 1.601-3.5625 2.6152-5.8906 2.6152-4.4 0-8-3.6-8-8 0-2.3281 1.0143-4.4252 2.6152-5.8906z" fill="#f4a331"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="m12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10z" fill="#ed484c"/><path d="m8 10h8c1.108 0 2 0.892 2 2s-0.892 2-2 2h-8c-1.108 0-2-0.892-2-2s0.892-2 2-2z" fill="#fdffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" style="paint-order:stroke markers fill"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="m12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 4a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6z" fill="#000"/></svg>
\ No newline at end of file
--- /dev/null
+<svg width="24" height="24" enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m8 16h8v-8h-8v8zm4-14c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10z" fill="#49B382"/></svg>
\ No newline at end of file