feat: Integrate notification management and preferences across platforms (#4819)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-16 20:17:34 -05:00 committed by GitHub
parent 0b2e89c46f
commit 8c964a15ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1304 additions and 61 deletions

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.desktop
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.koin.core.annotation.Single
import org.meshtastic.core.repository.Notification
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.repository.NotificationPrefs
import androidx.compose.ui.window.Notification as ComposeNotification
@Single
class DesktopNotificationManager(private val prefs: NotificationPrefs) : NotificationManager {
private val _notifications = MutableSharedFlow<ComposeNotification>(extraBufferCapacity = 10)
val notifications: SharedFlow<ComposeNotification> = _notifications.asSharedFlow()
override fun dispatch(notification: Notification) {
val enabled =
when (notification.category) {
Notification.Category.Message -> prefs.messagesEnabled.value
Notification.Category.NodeEvent -> prefs.nodeEventsEnabled.value
Notification.Category.Battery -> prefs.lowBatteryEnabled.value
Notification.Category.Alert -> true
Notification.Category.Service -> true
}
if (!enabled) return
val composeType =
when (notification.type) {
Notification.Type.None -> ComposeNotification.Type.None
Notification.Type.Info -> ComposeNotification.Type.Info
Notification.Type.Warning -> ComposeNotification.Type.Warning
Notification.Type.Error -> ComposeNotification.Type.Error
}
_notifications.tryEmit(ComposeNotification(notification.title, notification.message, composeType))
}
override fun cancel(id: Int) {
// Desktop Tray notifications cannot be cancelled once sent via TrayState
}
override fun cancelAll() {
// Desktop Tray notifications cannot be cleared once sent via TrayState
}
}

View file

@ -19,23 +19,43 @@ package org.meshtastic.desktop
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyShortcut
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Notification
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberTrayState
import androidx.compose.ui.window.rememberWindowState
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.first
import org.koin.core.context.startKoin
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.desktop.data.DesktopPreferencesDataSource
import org.meshtastic.desktop.di.desktopModule
import org.meshtastic.desktop.di.desktopPlatformModule
import org.meshtastic.desktop.radio.DesktopMeshServiceController
import org.meshtastic.desktop.ui.DesktopMainScreen
import org.meshtastic.desktop.ui.navSavedStateConfig
import java.util.Locale
/**
@ -54,7 +74,8 @@ import java.util.Locale
*/
private val LocalAppLocale = staticCompositionLocalOf { "" }
fun main() = application {
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun main() = application(exitProcessOnExit = false) {
Logger.i { "Meshtastic Desktop — Starting" }
val koinApp = remember { startKoin { modules(desktopPlatformModule(), desktopModule()) } }
@ -83,18 +104,133 @@ fun main() = application {
else -> isSystemInDarkTheme()
}
Window(
onCloseRequest = ::exitApplication,
title = "Meshtastic Desktop",
icon = painterResource("icon.png"),
state = rememberWindowState(width = 1024.dp, height = 768.dp),
) {
// Providing localePref via a staticCompositionLocalOf forces the entire subtree to
// recompose when the locale changes — CMP Resources' rememberResourceEnvironment then
// re-reads Locale.current and all stringResource() calls update. Unlike key(), this
// preserves remembered state (including the navigation backstack).
CompositionLocalProvider(LocalAppLocale provides localePref) {
AppTheme(darkTheme = isDarkTheme) { DesktopMainScreen() }
var isAppVisible by remember { mutableStateOf(true) }
var isWindowReady by remember { mutableStateOf(false) }
val trayState = rememberTrayState()
val appIcon = painterResource("icon.png")
val notificationManager = remember { koinApp.koin.get<DesktopNotificationManager>() }
val desktopPrefs = remember { koinApp.koin.get<DesktopPreferencesDataSource>() }
val windowState = rememberWindowState()
LaunchedEffect(Unit) {
notificationManager.notifications.collect { notification -> trayState.sendNotification(notification) }
}
LaunchedEffect(Unit) {
val initialWidth = desktopPrefs.windowWidth.first()
val initialHeight = desktopPrefs.windowHeight.first()
val initialX = desktopPrefs.windowX.first()
val initialY = desktopPrefs.windowY.first()
windowState.size = DpSize(initialWidth.dp, initialHeight.dp)
windowState.position =
if (!initialX.isNaN() && !initialY.isNaN()) {
WindowPosition(initialX.dp, initialY.dp)
} else {
WindowPosition(Alignment.Center)
}
isWindowReady = true
snapshotFlow {
val x = if (windowState.position.isSpecified) windowState.position.x.value else Float.NaN
val y = if (windowState.position.isSpecified) windowState.position.y.value else Float.NaN
listOf(windowState.size.width.value, windowState.size.height.value, x, y)
}
.collect { bounds ->
desktopPrefs.setWindowBounds(width = bounds[0], height = bounds[1], x = bounds[2], y = bounds[3])
}
}
Tray(
state = trayState,
icon = appIcon,
menu = {
Item("Show Meshtastic", onClick = { isAppVisible = true })
Item(
"Test Notification",
onClick = {
trayState.sendNotification(
Notification(
"Meshtastic",
"This is a test notification from the System Tray",
Notification.Type.Info,
),
)
},
)
Item("Quit", onClick = ::exitApplication)
},
)
if (isWindowReady && isAppVisible) {
Window(
onCloseRequest = { isAppVisible = false },
title = "Meshtastic Desktop",
icon = appIcon,
state = windowState,
) {
val backStack =
rememberNavBackStack(navSavedStateConfig, TopLevelDestination.Connections.route as NavKey)
MenuBar {
Menu("File") {
Item("Settings", shortcut = KeyShortcut(Key.Comma, meta = true)) {
if (
TopLevelDestination.Settings != TopLevelDestination.fromNavKey(backStack.lastOrNull())
) {
backStack.add(TopLevelDestination.Settings.route)
while (backStack.size > 1) {
backStack.removeAt(0)
}
}
}
Separator()
Item("Quit", shortcut = KeyShortcut(Key.Q, meta = true)) { exitApplication() }
}
Menu("View") {
Item("Toggle Theme", shortcut = KeyShortcut(Key.T, meta = true, shift = true)) {
val newTheme = if (isDarkTheme) 1 else 2 // 1 = Light, 2 = Dark
uiPrefs.setTheme(newTheme)
}
}
Menu("Navigate") {
Item("Conversations", shortcut = KeyShortcut(Key.One, meta = true)) {
backStack.add(TopLevelDestination.Conversations.route)
while (backStack.size > 1) {
backStack.removeAt(0)
}
}
Item("Nodes", shortcut = KeyShortcut(Key.Two, meta = true)) {
backStack.add(TopLevelDestination.Nodes.route)
while (backStack.size > 1) {
backStack.removeAt(0)
}
}
Item("Map", shortcut = KeyShortcut(Key.Three, meta = true)) {
backStack.add(TopLevelDestination.Map.route)
while (backStack.size > 1) {
backStack.removeAt(0)
}
}
Item("Connections", shortcut = KeyShortcut(Key.Four, meta = true)) {
backStack.add(TopLevelDestination.Connections.route)
while (backStack.size > 1) {
backStack.removeAt(0)
}
}
}
Menu("Help") { Item("About") { backStack.add(SettingsRoutes.About) } }
}
// Providing localePref via a staticCompositionLocalOf forces the entire subtree to
// recompose when the locale changes — CMP Resources' rememberResourceEnvironment then
// re-reads Locale.current and all stringResource() calls update. Unlike key(), this
// preserves remembered state (including the navigation backstack).
CompositionLocalProvider(LocalAppLocale provides localePref) {
AppTheme(darkTheme = isDarkTheme) { DesktopMainScreen(backStack) }
}
}
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.desktop.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
const val KEY_WINDOW_WIDTH = "window_width"
const val KEY_WINDOW_HEIGHT = "window_height"
const val KEY_WINDOW_X = "window_x"
const val KEY_WINDOW_Y = "window_y"
@Single
class DesktopPreferencesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore<Preferences>) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val windowWidth: StateFlow<Float> = dataStore.prefStateFlow(key = WINDOW_WIDTH, default = 1024f)
val windowHeight: StateFlow<Float> = dataStore.prefStateFlow(key = WINDOW_HEIGHT, default = 768f)
val windowX: StateFlow<Float> = dataStore.prefStateFlow(key = WINDOW_X, default = Float.NaN)
val windowY: StateFlow<Float> = dataStore.prefStateFlow(key = WINDOW_Y, default = Float.NaN)
fun setWindowBounds(width: Float, height: Float, x: Float, y: Float) {
scope.launch {
dataStore.edit { prefs ->
prefs[WINDOW_WIDTH] = width
prefs[WINDOW_HEIGHT] = height
prefs[WINDOW_X] = x
prefs[WINDOW_Y] = y
}
}
}
private fun <T : Any> DataStore<Preferences>.prefStateFlow(
key: Preferences.Key<T>,
default: T,
started: SharingStarted = SharingStarted.Lazily,
): StateFlow<T> = data.map { it[key] ?: default }.stateIn(scope = scope, started = started, initialValue = default)
companion object {
val WINDOW_WIDTH = floatPreferencesKey(KEY_WINDOW_WIDTH)
val WINDOW_HEIGHT = floatPreferencesKey(KEY_WINDOW_HEIGHT)
val WINDOW_X = floatPreferencesKey(KEY_WINDOW_X)
val WINDOW_Y = floatPreferencesKey(KEY_WINDOW_Y)
}
}

View file

@ -49,7 +49,6 @@ import org.meshtastic.desktop.stub.NoopLocationRepository
import org.meshtastic.desktop.stub.NoopMQTTRepository
import org.meshtastic.desktop.stub.NoopMagneticFieldProvider
import org.meshtastic.desktop.stub.NoopMeshLocationManager
import org.meshtastic.desktop.stub.NoopMeshServiceNotifications
import org.meshtastic.desktop.stub.NoopMeshWorkerManager
import org.meshtastic.desktop.stub.NoopPhoneLocationProvider
import org.meshtastic.desktop.stub.NoopPlatformAnalytics
@ -134,7 +133,9 @@ private fun desktopPlatformStubsModule() = module {
locationManager = get(),
)
}
single<MeshServiceNotifications> { NoopMeshServiceNotifications() }
single<MeshServiceNotifications> {
org.meshtastic.desktop.notification.DesktopMeshServiceNotifications(notificationManager = get())
}
single<PlatformAnalytics> { NoopPlatformAnalytics() }
single<ServiceBroadcasts> { NoopServiceBroadcasts() }
single<AppWidgetUpdater> { NoopAppWidgetUpdater() }

View file

@ -155,9 +155,9 @@ fun desktopPlatformModule() = module {
override val isDebug: Boolean = true
override val applicationId: String = "org.meshtastic.desktop"
override val versionCode: Int = 1
override val versionName: String = "0.1.0-desktop"
override val absoluteMinFwVersion: String = "2.0.0"
override val minFwVersion: String = "2.5.0"
override val versionName: String = "2.7.14"
override val absoluteMinFwVersion: String = "2.3.15"
override val minFwVersion: String = "2.5.14"
}
}

View file

@ -0,0 +1,160 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.desktop.notification
import org.koin.core.annotation.Single
import org.meshtastic.core.model.Node
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.Notification
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.low_battery_message
import org.meshtastic.core.resources.low_battery_title
import org.meshtastic.core.resources.new_node_seen
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.Telemetry
@Single
@Suppress("TooManyFunctions")
class DesktopMeshServiceNotifications(private val notificationManager: NotificationManager) : MeshServiceNotifications {
override fun clearNotifications() {
notificationManager.cancelAll()
}
override fun initChannels() {
// no-op for desktop
}
override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Any {
// We don't have a foreground service on desktop
return Unit
}
override suspend fun updateMessageNotification(
contactKey: String,
name: String,
message: String,
isBroadcast: Boolean,
channelName: String?,
isSilent: Boolean,
) {
notificationManager.dispatch(
Notification(
title = name,
message = message,
category = Notification.Category.Message,
contactKey = contactKey,
isSilent = isSilent,
id = contactKey.hashCode(),
),
)
}
override suspend fun updateWaypointNotification(
contactKey: String,
name: String,
message: String,
waypointId: Int,
isSilent: Boolean,
) {
notificationManager.dispatch(
Notification(
title = name,
message = message,
category = Notification.Category.Message,
contactKey = contactKey,
isSilent = isSilent,
),
)
}
override suspend fun updateReactionNotification(
contactKey: String,
name: String,
emoji: String,
isBroadcast: Boolean,
channelName: String?,
isSilent: Boolean,
) {
notificationManager.dispatch(
Notification(
title = name,
message = emoji,
category = Notification.Category.Message,
contactKey = contactKey,
isSilent = isSilent,
),
)
}
@Suppress("ktlint:standard:max-line-length")
override fun showAlertNotification(contactKey: String, name: String, alert: String) {
notificationManager.dispatch(
Notification(
title = name,
message = alert,
category = Notification.Category.Alert,
contactKey = contactKey,
),
)
}
override fun showNewNodeSeenNotification(node: Node) {
notificationManager.dispatch(
Notification(
title = getString(Res.string.new_node_seen, node.user.short_name),
message = node.user.long_name,
category = Notification.Category.NodeEvent,
),
)
}
override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {
notificationManager.dispatch(
Notification(
title = getString(Res.string.low_battery_title, node.user.short_name),
message = getString(Res.string.low_battery_message, node.user.long_name, node.batteryLevel ?: 0),
category = Notification.Category.Battery,
id = node.num,
),
)
}
override fun showClientNotification(clientNotification: ClientNotification) {
notificationManager.dispatch(
Notification(
title = "Meshtastic",
message = clientNotification.message,
category = Notification.Category.Alert,
id = clientNotification.toString().hashCode(),
),
)
}
override fun cancelMessageNotification(contactKey: String) {
notificationManager.cancel(contactKey.hashCode())
}
override fun cancelLowBatteryNotification(node: Node) {
notificationManager.cancel(node.num)
}
override fun clearClientNotification(notification: ClientNotification) {
notificationManager.cancel(notification.toString().hashCode())
}
}

View file

@ -28,9 +28,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.savedstate.serialization.SavedStateConfiguration
import kotlinx.serialization.modules.SerializersModule
@ -55,7 +55,7 @@ import org.meshtastic.desktop.navigation.desktopNavGraph
* Polymorphic serialization configuration for Navigation 3 saved-state support. Registers all route types used in the
* desktop navigation graph.
*/
private val navSavedStateConfig = SavedStateConfiguration {
internal val navSavedStateConfig = SavedStateConfiguration {
serializersModule = SerializersModule {
polymorphic(NavKey::class) {
// Nodes
@ -142,8 +142,7 @@ private val navSavedStateConfig = SavedStateConfiguration {
* app, proving the shared backstack architecture works across targets.
*/
@Composable
fun DesktopMainScreen(radioService: RadioInterfaceService = koinInject()) {
val backStack = rememberNavBackStack(navSavedStateConfig, NodesRoutes.NodesGraph as NavKey)
fun DesktopMainScreen(backStack: NavBackStack<NavKey>, radioService: RadioInterfaceService = koinInject()) {
val currentKey = backStack.lastOrNull()
val selected = TopLevelDestination.fromNavKey(currentKey)
@ -159,8 +158,10 @@ fun DesktopMainScreen(radioService: RadioInterfaceService = koinInject()) {
selected = destination == selected,
onClick = {
if (destination != selected) {
backStack.clear()
backStack.add(destination.route)
while (backStack.size > 1) {
backStack.removeAt(0)
}
}
},
icon = {

View file

@ -74,6 +74,7 @@ import org.meshtastic.core.ui.util.rememberShowToastResource
import org.meshtastic.feature.settings.SettingsViewModel
import org.meshtastic.feature.settings.component.ExpressiveSection
import org.meshtastic.feature.settings.component.HomoglyphSetting
import org.meshtastic.feature.settings.component.NotificationSection
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.ModuleRoute
import org.meshtastic.feature.settings.radio.RadioConfigItemList
@ -202,6 +203,15 @@ fun DesktopSettingsScreen(
)
}
NotificationSection(
messagesEnabled = settingsViewModel.messagesEnabled.collectAsStateWithLifecycle().value,
onToggleMessages = { settingsViewModel.setMessagesEnabled(it) },
nodeEventsEnabled = settingsViewModel.nodeEventsEnabled.collectAsStateWithLifecycle().value,
onToggleNodeEvents = { settingsViewModel.setNodeEventsEnabled(it) },
lowBatteryEnabled = settingsViewModel.lowBatteryEnabled.collectAsStateWithLifecycle().value,
onToggleLowBattery = { settingsViewModel.setLowBatteryEnabled(it) },
)
DesktopAppInfoSection(
appVersionName = settingsViewModel.appVersionName,
excludedModulesUnlocked = excludedModulesUnlocked,

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="24" height="24" viewBox="0 0 100 55" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.802386,0,0,0.460028,-421.748,-122.127)">
<g transform="matrix(0.579082,0,0,1.01004,460.975,-39.6867)">
<path d="M250.908,330.267L193.126,415.005L180.938,406.694L244.802,313.037C246.174,311.024 248.453,309.819 250.889,309.816C253.326,309.814 255.606,311.015 256.982,313.026L320.994,406.536L308.821,414.869L250.908,330.267Z"/>
</g>
<g transform="matrix(0.582378,0,0,1.01579,485.019,-211.182)">
<path d="M87.642,581.398L154.757,482.977L142.638,474.713L75.523,573.134L87.642,581.398Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="24" height="24" viewBox="0 0 100 55" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.802386,0,0,0.460028,-421.748,-122.127)">
<g transform="matrix(0.579082,0,0,1.01004,460.975,-39.6867)">
<path d="M250.908,330.267L193.126,415.005L180.938,406.694L244.802,313.037C246.174,311.024 248.453,309.819 250.889,309.816C253.326,309.814 255.606,311.015 256.982,313.026L320.994,406.536L308.821,414.869L250.908,330.267Z" style="fill:white;"/>
</g>
<g transform="matrix(0.582378,0,0,1.01579,485.019,-211.182)">
<path d="M87.642,581.398L154.757,482.977L142.638,474.713L75.523,573.134L87.642,581.398Z" style="fill:white;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB