mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 02:50:00 +08:00
FEATURE: other notifications tab for redesign user menu (#18164)
This commit adds to the experimental user menu a new "other notifications" tab that's very similar to the "all notifications" tab, but with the main difference being that it doesn't show notification types that do have dedicated tabs in the menu (e.g. mentions, likes, replies etc.). The rationale behind this is that the notification types that do have dedicated tabs tend to dominate the "all notifications" tab, leaving very small chances for the user to notice rarer or infrequent notification types. Adding a tab for all the other types gives the user a way to review those infrequent notification types. Internal ticket: t72978. Co-authored-by: OsamaSayegh <asooomaasoooma90@gmail.com>
This commit is contained in:
parent
bf6c9e0f28
commit
661a903a0b
|
@ -1,13 +1,6 @@
|
||||||
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
|
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
|
||||||
|
|
||||||
export default class UserMenuLikesNotificationsList extends UserMenuNotificationsList {
|
export default class UserMenuLikesNotificationsList extends UserMenuNotificationsList {
|
||||||
get filterByTypes() {
|
|
||||||
// TODO(osama): reaction is a type used by the reactions plugin, but it's
|
|
||||||
// added here temporarily unitl we add a plugin API for extending
|
|
||||||
// filterByTypes in lists
|
|
||||||
return ["liked", "liked_consolidated", "reaction"];
|
|
||||||
}
|
|
||||||
|
|
||||||
get dismissTypes() {
|
get dismissTypes() {
|
||||||
return this.filterByTypes;
|
return this.filterByTypes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
|
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
|
||||||
|
|
||||||
export default class UserMenuMentionsNotificationsList extends UserMenuNotificationsList {
|
export default class UserMenuMentionsNotificationsList extends UserMenuNotificationsList {
|
||||||
get filterByTypes() {
|
|
||||||
return ["mentioned"];
|
|
||||||
}
|
|
||||||
|
|
||||||
get dismissTypes() {
|
get dismissTypes() {
|
||||||
return this.filterByTypes;
|
return this.filterByTypes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
class="quick-access-panel"
|
class="quick-access-panel"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-labelledby={{concat "user-menu-button-" this.currentTabId}}>
|
aria-labelledby={{concat "user-menu-button-" this.currentTabId}}>
|
||||||
{{component this.currentPanelComponent closeUserMenu=@closeUserMenu}}
|
{{component this.currentPanelComponent closeUserMenu=@closeUserMenu filterByTypes=this.currentNotificationTypes}}
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-tabs-container" role="tablist" aria-orientation="vertical" aria-label={{i18n "user_menu.sr_menu_tabs"}}>
|
<div class="menu-tabs-container" role="tablist" aria-orientation="vertical" aria-label={{i18n "user_menu.sr_menu_tabs"}}>
|
||||||
<div class="top-tabs tabs-list">
|
<div class="top-tabs tabs-list">
|
||||||
|
|
|
@ -41,6 +41,10 @@ const CORE_TOP_TABS = [
|
||||||
get count() {
|
get count() {
|
||||||
return this.getUnreadCountForType("replied");
|
return this.getUnreadCountForType("replied");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notificationTypes() {
|
||||||
|
return ["replied"];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
class extends UserMenuTab {
|
class extends UserMenuTab {
|
||||||
|
@ -59,6 +63,10 @@ const CORE_TOP_TABS = [
|
||||||
get count() {
|
get count() {
|
||||||
return this.getUnreadCountForType("mentioned");
|
return this.getUnreadCountForType("mentioned");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notificationTypes() {
|
||||||
|
return ["mentioned"];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
class extends UserMenuTab {
|
class extends UserMenuTab {
|
||||||
|
@ -81,6 +89,13 @@ const CORE_TOP_TABS = [
|
||||||
get count() {
|
get count() {
|
||||||
return this.getUnreadCountForType("liked");
|
return this.getUnreadCountForType("liked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(osama): reaction is a type used by the reactions plugin, but it's
|
||||||
|
// added here temporarily unitl we add a plugin API for extending
|
||||||
|
// filterByTypes in lists
|
||||||
|
get notificationTypes() {
|
||||||
|
return ["liked", "liked_consolidated", "reaction"];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
class extends UserMenuTab {
|
class extends UserMenuTab {
|
||||||
|
@ -105,6 +120,9 @@ const CORE_TOP_TABS = [
|
||||||
this.siteSettings.enable_personal_messages || this.currentUser.staff
|
this.siteSettings.enable_personal_messages || this.currentUser.staff
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
get notificationTypes() {
|
||||||
|
return ["private_message"];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
class extends UserMenuTab {
|
class extends UserMenuTab {
|
||||||
|
@ -123,6 +141,10 @@ const CORE_TOP_TABS = [
|
||||||
get count() {
|
get count() {
|
||||||
return this.getUnreadCountForType("bookmark_reminder");
|
return this.getUnreadCountForType("bookmark_reminder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notificationTypes() {
|
||||||
|
return ["bookmark_reminder"];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
class extends UserMenuTab {
|
class extends UserMenuTab {
|
||||||
|
@ -164,6 +186,35 @@ const CORE_BOTTOM_TABS = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CORE_OTHER_NOTIFICATIONS_TAB = class extends UserMenuTab {
|
||||||
|
constructor(currentUser, siteSettings, site, otherNotificationTypes) {
|
||||||
|
super(...arguments);
|
||||||
|
this.otherNotificationTypes = otherNotificationTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
return "discourse-other-tab";
|
||||||
|
}
|
||||||
|
|
||||||
|
get panelComponent() {
|
||||||
|
return "user-menu/other-notifications-list";
|
||||||
|
}
|
||||||
|
|
||||||
|
get count() {
|
||||||
|
return this.otherNotificationTypes.reduce((sum, notificationType) => {
|
||||||
|
return sum + this.getUnreadCountForType(notificationType);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get notificationTypes() {
|
||||||
|
return this.otherNotificationTypes;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default class UserMenu extends Component {
|
export default class UserMenu extends Component {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
|
@ -172,6 +223,7 @@ export default class UserMenu extends Component {
|
||||||
|
|
||||||
@tracked currentTabId = DEFAULT_TAB_ID;
|
@tracked currentTabId = DEFAULT_TAB_ID;
|
||||||
@tracked currentPanelComponent = DEFAULT_PANEL_COMPONENT;
|
@tracked currentPanelComponent = DEFAULT_PANEL_COMPONENT;
|
||||||
|
@tracked currentNotificationTypes;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
@ -196,7 +248,6 @@ export default class UserMenu extends Component {
|
||||||
CUSTOM_TABS_CLASSES.forEach((tabClass) => {
|
CUSTOM_TABS_CLASSES.forEach((tabClass) => {
|
||||||
const tab = new tabClass(this.currentUser, this.siteSettings, this.site);
|
const tab = new tabClass(this.currentUser, this.siteSettings, this.site);
|
||||||
if (tab.shouldDisplay) {
|
if (tab.shouldDisplay) {
|
||||||
// ensure the review queue tab is always last
|
|
||||||
if (reviewQueueTabIndex === -1) {
|
if (reviewQueueTabIndex === -1) {
|
||||||
tabs.push(tab);
|
tabs.push(tab);
|
||||||
} else {
|
} else {
|
||||||
|
@ -206,6 +257,15 @@ export default class UserMenu extends Component {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tabs.push(
|
||||||
|
new CORE_OTHER_NOTIFICATIONS_TAB(
|
||||||
|
this.currentUser,
|
||||||
|
this.siteSettings,
|
||||||
|
this.site,
|
||||||
|
this.#notificationTypesForTheOtherTab(tabs)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return tabs.map((tab, index) => {
|
return tabs.map((tab, index) => {
|
||||||
tab.position = index;
|
tab.position = index;
|
||||||
return tab;
|
return tab;
|
||||||
|
@ -229,14 +289,14 @@ export default class UserMenu extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get _coreBottomTabs() {
|
#notificationTypesForTheOtherTab(tabs) {
|
||||||
return [
|
const usedNotificationTypes = tabs
|
||||||
{
|
.filter((tab) => tab.notificationTypes)
|
||||||
id: "preferences",
|
.map((tab) => tab.notificationTypes)
|
||||||
icon: "user-cog",
|
.flat();
|
||||||
href: `${this.currentUser.path}/preferences`,
|
return Object.keys(this.site.notification_types).filter(
|
||||||
},
|
(notificationType) => !usedNotificationTypes.includes(notificationType)
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -244,6 +304,7 @@ export default class UserMenu extends Component {
|
||||||
if (this.currentTabId !== tab.id) {
|
if (this.currentTabId !== tab.id) {
|
||||||
this.currentTabId = tab.id;
|
this.currentTabId = tab.id;
|
||||||
this.currentPanelComponent = tab.panelComponent;
|
this.currentPanelComponent = tab.panelComponent;
|
||||||
|
this.currentNotificationTypes = tab.notificationTypes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default class UserMenuNotificationsList extends UserMenuItemsList {
|
||||||
@service store;
|
@service store;
|
||||||
|
|
||||||
get filterByTypes() {
|
get filterByTypes() {
|
||||||
return null;
|
return this.args.filterByTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
get dismissTypes() {
|
get dismissTypes() {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class UserMenuOtherNotificationsList extends UserMenuNotificationsList {
|
||||||
|
@service currentUser;
|
||||||
|
@service siteSettings;
|
||||||
|
@service site;
|
||||||
|
|
||||||
|
get dismissTypes() {
|
||||||
|
return this.filterByTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissWarningModal() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,6 @@
|
||||||
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
|
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
|
||||||
|
|
||||||
export default class UserMenuRepliesNotificationsList extends UserMenuNotificationsList {
|
export default class UserMenuRepliesNotificationsList extends UserMenuNotificationsList {
|
||||||
get filterByTypes() {
|
|
||||||
return ["replied"];
|
|
||||||
}
|
|
||||||
|
|
||||||
get dismissTypes() {
|
get dismissTypes() {
|
||||||
return this.filterByTypes;
|
return this.filterByTypes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,11 @@ export default class UserMenuTab {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Array} Notification types displayed in tab. Those notifications will be removed from "other" tab.
|
||||||
|
*/
|
||||||
|
get notificationTypes() {}
|
||||||
|
|
||||||
getUnreadCountForType(type) {
|
getUnreadCountForType(type) {
|
||||||
const key = `grouped_unread_notifications.${this.site.notification_types[type]}`;
|
const key = `grouped_unread_notifications.${this.site.notification_types[type]}`;
|
||||||
// we're retrieving the value with get() so that Ember tracks the property
|
// we're retrieving the value with get() so that Ember tracks the property
|
||||||
|
|
|
@ -135,6 +135,7 @@ acceptance("User menu", function (needs) {
|
||||||
"user-menu-button-custom-tab-1": "6",
|
"user-menu-button-custom-tab-1": "6",
|
||||||
"user-menu-button-custom-tab-2": "7",
|
"user-menu-button-custom-tab-2": "7",
|
||||||
"user-menu-button-review-queue": "8",
|
"user-menu-button-review-queue": "8",
|
||||||
|
"user-menu-button-other": "9",
|
||||||
};
|
};
|
||||||
|
|
||||||
await visit("/");
|
await visit("/");
|
||||||
|
@ -161,7 +162,7 @@ acceptance("User menu", function (needs) {
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
query(".tabs-list.bottom-tabs .btn").dataset.tabNumber,
|
query(".tabs-list.bottom-tabs .btn").dataset.tabNumber,
|
||||||
"9",
|
"10",
|
||||||
"bottom tab has the correct data-tab-number"
|
"bottom tab has the correct data-tab-number"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -528,6 +529,8 @@ acceptance("User menu - Dismiss button", function (needs) {
|
||||||
grouped_unread_notifications: {
|
grouped_unread_notifications: {
|
||||||
[NOTIFICATION_TYPES.bookmark_reminder]: 103,
|
[NOTIFICATION_TYPES.bookmark_reminder]: 103,
|
||||||
[NOTIFICATION_TYPES.private_message]: 89,
|
[NOTIFICATION_TYPES.private_message]: 89,
|
||||||
|
[NOTIFICATION_TYPES.votes_released]: 1,
|
||||||
|
[NOTIFICATION_TYPES.code_review_commit_approved]: 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -713,4 +716,27 @@ acceptance("User menu - Dismiss button", function (needs) {
|
||||||
"mark-read request is sent without a confirmation modal"
|
"mark-read request is sent without a confirmation modal"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("doesn't show confirmation modal for the other notifications list", async function (assert) {
|
||||||
|
await visit("/");
|
||||||
|
await click(".d-header-icons .current-user");
|
||||||
|
|
||||||
|
await click("#user-menu-button-other");
|
||||||
|
let repliesBadgeNotification = query(
|
||||||
|
"#user-menu-button-other .badge-notification"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
repliesBadgeNotification.textContent.trim(),
|
||||||
|
"4",
|
||||||
|
"badge shows the right count"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".user-menu .notifications-dismiss");
|
||||||
|
|
||||||
|
assert.ok(!exists("#user-menu-button-other .badge-notification"));
|
||||||
|
assert.ok(
|
||||||
|
markRead,
|
||||||
|
"mark-read request is sent without a confirmation modal"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -162,6 +162,7 @@ module("Integration | Component | site-header", function (hooks) {
|
||||||
await triggerKeyEvent(document, "keydown", "ArrowDown");
|
await triggerKeyEvent(document, "keydown", "ArrowDown");
|
||||||
await triggerKeyEvent(document, "keydown", "ArrowDown");
|
await triggerKeyEvent(document, "keydown", "ArrowDown");
|
||||||
await triggerKeyEvent(document, "keydown", "ArrowDown");
|
await triggerKeyEvent(document, "keydown", "ArrowDown");
|
||||||
|
await triggerKeyEvent(document, "keydown", "ArrowDown");
|
||||||
|
|
||||||
focusedTab = document.activeElement;
|
focusedTab = document.activeElement;
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
|
|
|
@ -48,7 +48,7 @@ module("Integration | Component | user-menu", function (hooks) {
|
||||||
test("the menu has a group of tabs at the top", async function (assert) {
|
test("the menu has a group of tabs at the top", async function (assert) {
|
||||||
await render(template);
|
await render(template);
|
||||||
const tabs = queryAll(".top-tabs.tabs-list .btn");
|
const tabs = queryAll(".top-tabs.tabs-list .btn");
|
||||||
assert.strictEqual(tabs.length, 6);
|
assert.strictEqual(tabs.length, 7);
|
||||||
[
|
[
|
||||||
"all-notifications",
|
"all-notifications",
|
||||||
"replies",
|
"replies",
|
||||||
|
@ -72,7 +72,7 @@ module("Integration | Component | user-menu", function (hooks) {
|
||||||
assert.strictEqual(tabs.length, 1);
|
assert.strictEqual(tabs.length, 1);
|
||||||
const profileTab = tabs[0];
|
const profileTab = tabs[0];
|
||||||
assert.strictEqual(profileTab.id, "user-menu-button-profile");
|
assert.strictEqual(profileTab.id, "user-menu-button-profile");
|
||||||
assert.strictEqual(profileTab.dataset.tabNumber, "6");
|
assert.strictEqual(profileTab.dataset.tabNumber, "7");
|
||||||
assert.strictEqual(profileTab.getAttribute("tabindex"), "-1");
|
assert.strictEqual(profileTab.getAttribute("tabindex"), "-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,11 +82,11 @@ module("Integration | Component | user-menu", function (hooks) {
|
||||||
assert.ok(!exists("#user-menu-button-likes"));
|
assert.ok(!exists("#user-menu-button-likes"));
|
||||||
|
|
||||||
const tabs = Array.from(queryAll(".tabs-list .btn")); // top and bottom tabs
|
const tabs = Array.from(queryAll(".tabs-list .btn")); // top and bottom tabs
|
||||||
assert.strictEqual(tabs.length, 6);
|
assert.strictEqual(tabs.length, 7);
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
tabs.map((t) => t.dataset.tabNumber),
|
tabs.map((t) => t.dataset.tabNumber),
|
||||||
["0", "1", "2", "3", "4", "5"],
|
["0", "1", "2", "3", "4", "5", "6"],
|
||||||
"data-tab-number of the tabs has no gaps when the likes tab is hidden"
|
"data-tab-number of the tabs has no gaps when the likes tab is hidden"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -98,11 +98,11 @@ module("Integration | Component | user-menu", function (hooks) {
|
||||||
assert.strictEqual(tab.dataset.tabNumber, "6");
|
assert.strictEqual(tab.dataset.tabNumber, "6");
|
||||||
|
|
||||||
const tabs = Array.from(queryAll(".tabs-list .btn")); // top and bottom tabs
|
const tabs = Array.from(queryAll(".tabs-list .btn")); // top and bottom tabs
|
||||||
assert.strictEqual(tabs.length, 8);
|
assert.strictEqual(tabs.length, 9);
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
tabs.map((t) => t.dataset.tabNumber),
|
tabs.map((t) => t.dataset.tabNumber),
|
||||||
["0", "1", "2", "3", "4", "5", "6", "7"],
|
["0", "1", "2", "3", "4", "5", "6", "7", "8"],
|
||||||
"data-tab-number of the tabs has no gaps when the reviewables tab is show"
|
"data-tab-number of the tabs has no gaps when the reviewables tab is show"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -117,11 +117,11 @@ module("Integration | Component | user-menu", function (hooks) {
|
||||||
assert.ok(!exists("#user-menu-button-messages"));
|
assert.ok(!exists("#user-menu-button-messages"));
|
||||||
|
|
||||||
const tabs = Array.from(queryAll(".tabs-list .btn")); // top and bottom tabs
|
const tabs = Array.from(queryAll(".tabs-list .btn")); // top and bottom tabs
|
||||||
assert.strictEqual(tabs.length, 6);
|
assert.strictEqual(tabs.length, 7);
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
tabs.map((t) => t.dataset.tabNumber),
|
tabs.map((t) => t.dataset.tabNumber),
|
||||||
["0", "1", "2", "3", "4", "5"],
|
["0", "1", "2", "3", "4", "5", "6"],
|
||||||
"data-tab-number of the tabs has no gaps when the messages tab is hidden"
|
"data-tab-number of the tabs has no gaps when the messages tab is hidden"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -65,6 +65,7 @@ module SvgSprite
|
||||||
"discourse-compress",
|
"discourse-compress",
|
||||||
"discourse-emojis",
|
"discourse-emojis",
|
||||||
"discourse-expand",
|
"discourse-expand",
|
||||||
|
"discourse-other-tab",
|
||||||
"download",
|
"download",
|
||||||
"ellipsis-h",
|
"ellipsis-h",
|
||||||
"ellipsis-v",
|
"ellipsis-v",
|
||||||
|
|
13
vendor/assets/svg-icons/discourse-additional.svg
vendored
13
vendor/assets/svg-icons/discourse-additional.svg
vendored
|
@ -40,4 +40,17 @@ Additional SVG icons
|
||||||
<path class="svg-arrow" d="M0 6s1.796-.013 4.67-3.615C5.851.9 6.93.006 8 0c1.07-.006 2.148.887 3.343 2.385C14.233 6.005 16 6 16 6H0z"/>
|
<path class="svg-arrow" d="M0 6s1.796-.013 4.67-3.615C5.851.9 6.93.006 8 0c1.07-.006 2.148.887 3.343 2.385C14.233 6.005 16 6 16 6H0z"/>
|
||||||
<path class="svg-content" d="m0 7s2 0 5-4c1-1 2-2 3-2 1 0 2 1 3 2 3 4 5 4 5 4h-16z"/>
|
<path class="svg-content" d="m0 7s2 0 5-4c1-1 2-2 3-2 1 0 2 1 3 2 3 4 5 4 5 4h-16z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id='discourse-other-tab' viewBox="0 0 114 113">
|
||||||
|
<g clip-path="url(#clip0_2925_742)">
|
||||||
|
<rect x="8" y="8" width="44" height="44" rx="5"/>
|
||||||
|
<rect x="8" y="61" width="44" height="44" rx="5"/>
|
||||||
|
<rect x="62" y="61" width="44" height="44" rx="5"/>
|
||||||
|
<rect width="44" height="43.9967" rx="5" transform="matrix(0.705436 -0.708774 0.705436 0.708774 53 30)"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2925_742">
|
||||||
|
<rect width="114" height="113"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 7.3 KiB |
Loading…
Reference in New Issue
Block a user