From b282ac32afa059a24e9da94eb66f31628ce7686e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 08:47:43 +0000 Subject: [PATCH] feat: turn Notify chips into bell mute-toggles listing all thread members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composer's Notify row previously showed only the parent's p tags and its author, each with an x that removed the user for good. It now lists every thread member (authors along the reply chain plus the parent's mentions), and each chip carries a bell instead: tapping it mutes the notification for that user but keeps the chip — faded and with a bell-off icon so the state is obvious — making it one tap to add them back. Muted members are dropped from the outgoing event's p tags (and from a private note's receivers), and drafts round-trip the muted state by re-deriving thread members whose p tag the draft dropped. The NIP-22 comment composer gets the same chip semantics; there the mute is honored for the optional zap-sender tag, while the structural root/reply scope tags stay as NIP-22 requires. Adds the notifications_off glyph to the Material Symbols subset font. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013eK8mGPcuKYXNyhKVJ8Wg7 --- .../ui/note/creators/notify/Notifying.kt | 21 +++-- .../nip22Comments/CommentPostViewModel.kt | 38 ++++++--- .../nip22Comments/GenericCommentPostScreen.kt | 9 +- .../loggedIn/home/ShortNotePostScreen.kt | 6 +- .../loggedIn/home/ShortNotePostViewModel.kt | 79 +++++++++++++----- amethyst/src/main/res/values/strings.xml | 2 + .../font/material_symbols_outlined.ttf | Bin 464004 -> 467128 bytes .../commons/icons/symbols/MaterialSymbols.kt | 1 + 8 files changed, 114 insertions(+), 42 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt index 64f1d09337..b7c1ca7a31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -49,7 +50,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size24dp import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf @OptIn(ExperimentalLayoutApi::class) @Composable @@ -58,8 +62,9 @@ fun Notifying( accountViewModel: AccountViewModel, label: String? = null, showWhenEmpty: Boolean = false, + mutedNotifies: ImmutableSet = persistentSetOf(), onAddUser: (() -> Unit)? = null, - onClick: (User) -> Unit, + onToggleNotify: (User) -> Unit, ) { val mentions = baseMentions?.toSet() @@ -83,7 +88,7 @@ fun Notifying( // spacing matches the horizontal spacing between chips. CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { mentions?.forEach { user -> - NotifyUserChip(user, accountViewModel) { onClick(user) } + NotifyUserChip(user, user.pubkeyHex in mutedNotifies, accountViewModel) { onToggleNotify(user) } } if (onAddUser != null) { @@ -97,12 +102,16 @@ fun Notifying( @Composable private fun NotifyUserChip( user: User, + isMuted: Boolean, accountViewModel: AccountViewModel, - onRemove: () -> Unit, + onToggleNotify: () -> Unit, ) { InputChip( selected = false, - onClick = onRemove, + onClick = onToggleNotify, + // The bell-off icon alone is easy to miss at chip size, so a muted member + // also fades as a second cue while staying in the list for easy re-adding. + modifier = if (isMuted) Modifier.alpha(0.4f) else Modifier, label = { UsernameDisplay( user, @@ -119,8 +128,8 @@ private fun NotifyUserChip( }, trailingIcon = { Icon( - symbol = MaterialSymbols.Close, - contentDescription = stringRes(R.string.notify_remove_user), + symbol = if (isMuted) MaterialSymbols.NotificationsOff else MaterialSymbols.Notifications, + contentDescription = stringRes(if (isMuted) R.string.notify_unmute_user else R.string.notify_mute_user), modifier = Modifier.size(InputChipDefaults.IconSize), ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 3c6b6d9b79..0183fee6c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -74,6 +74,7 @@ import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate @@ -177,6 +178,21 @@ open class CommentPostViewModel : var notifying by mutableStateOf?>(null) + // Members of the notifying list whose bell is off: they keep their chip + // (so they are one tap away from being added back) but are dropped from + // the extra notification p tags of the outgoing comment. + var mutedNotifies by mutableStateOf>(emptySet()) + + fun toggleNotify(user: User) { + mutedNotifies = + if (user.pubkeyHex in mutedNotifies) { + mutedNotifies - user.pubkeyHex + } else { + mutedNotifies + user.pubkeyHex + } + draftTag.newVersion() + } + // NIP-9B: latest community rules document for the community we're posting into. // Null when the reply target is not a community, or no rules have been observed yet. var communityRules: CommunityRulesEvent? by mutableStateOf(null) @@ -303,6 +319,7 @@ open class CommentPostViewModel : open fun reply(post: Note) { this.replyingTo = post this.externalIdentity = (post.event as? CommentEvent)?.scope() + mutedNotifies = emptySet() (post.event as? LnZapEvent)?.let { zap -> notifying = listOfNotNull(zapSenderToNotify(zap)) } @@ -498,14 +515,16 @@ open class CommentPostViewModel : notifying = draftEvent.rootAuthorKeys().mapNotNull { LocalCache.checkGetOrCreateUser(it) } + draftEvent.replyAuthorKeys().mapNotNull { LocalCache.checkGetOrCreateUser(it) } + mutedNotifies = emptySet() // Replies to zaps notify the zap sender through a plain p tag (the receipt's - // author keys above are the lightning provider). Restore the sender chip only - // if the draft still tags them — its absence means the user removed it. + // author keys above are the lightning provider). The sender chip always comes + // back; a missing p tag in the draft means the user muted their bell. (replyingTo?.event as? LnZapEvent)?.let { zap -> zapSenderToNotify(zap)?.let { sender -> - if (draftEvent.tags.mapNotNull(PTag::parseKey).contains(sender.pubkeyHex)) { - notifying = (notifying ?: emptyList()) + sender + notifying = ((notifying ?: emptyList()) + sender).distinct() + if (!draftEvent.tags.mapNotNull(PTag::parseKey).contains(sender.pubkeyHex)) { + mutedNotifies = mutedNotifies + sender.pubkeyHex } } } @@ -665,9 +684,9 @@ open class CommentPostViewModel : } } else if (replyingToEvent is LnZapEvent) { val sender = zapSenderToNotify(replyingToEvent) - // notifying starts with the sender; a missing entry means the - // user removed the chip, so respect that and don't tag them. - if (sender != null && notifying?.contains(sender) != false) { + // The sender's chip stays in the list; a muted bell means + // the user doesn't want to ping them, so don't tag them. + if (sender != null && sender.pubkeyHex !in mutedNotifies) { listOf(sender.toPTag()) } else { emptyList() @@ -855,6 +874,7 @@ open class CommentPostViewModel : mediaUploadTracker.finishUpload() notifying = null + mutedNotifies = emptySet() wantsInvoice = false wantsZapraiser = false @@ -886,10 +906,6 @@ open class CommentPostViewModel : this.multiOrchestrator?.remove(selected) } - open fun removeFromReplyList(userToRemove: User) { - notifying = notifying?.filter { it != userToRemove } - } - override fun onMessageChanged() { urlPreviews.update(message.text.toString()) revalidateDraft() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 8d4497e18d..9d06d1f355 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -257,8 +258,12 @@ private fun GenericCommentPostBody( } Row { - Notifying(postViewModel.notifying?.toImmutableList(), accountViewModel) { - postViewModel.removeFromReplyList(it) + Notifying( + baseMentions = postViewModel.notifying?.toImmutableList(), + accountViewModel = accountViewModel, + mutedNotifies = postViewModel.mutedNotifies.toImmutableSet(), + ) { + postViewModel.toggleNotify(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 534927c0f1..a9cb01b725 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -142,6 +142,7 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.launch @@ -327,9 +328,10 @@ private fun NewPostScreenBody( accountViewModel = accountViewModel, label = if (postViewModel.wantsPrivateNote) stringRes(R.string.private_note_visible_to) else null, showWhenEmpty = postViewModel.wantsPrivateNote, + mutedNotifies = postViewModel.mutedNotifies.toImmutableSet(), onAddUser = { postViewModel.wantsToAddNotifyUser = !postViewModel.wantsToAddNotifyUser }, ) { - postViewModel.removeFromReplyList(it) + postViewModel.toggleNotify(it) } } @@ -350,7 +352,7 @@ private fun NewPostScreenBody( ) } - if (postViewModel.wantsPrivateNote && postViewModel.pTags.isNullOrEmpty()) { + if (postViewModel.wantsPrivateNote && postViewModel.activeNotifies().isNullOrEmpty()) { Text( text = stringRes(R.string.private_note_no_receivers), style = MaterialTheme.typography.bodySmall, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 3cea485aef..edc2f40266 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -367,10 +367,29 @@ open class ShortNotePostViewModel : } } + // Members of pTags whose bell is off: they keep their chip in the Notify + // list (so they are one tap away from being added back) but are dropped + // from the outgoing event's p tags. + var mutedNotifies by mutableStateOf>(emptySet()) + + fun toggleNotify(user: User) { + mutedNotifies = + if (user.pubkeyHex in mutedNotifies) { + mutedNotifies - user.pubkeyHex + } else { + mutedNotifies + user.pubkeyHex + } + draftTag.newVersion() + } + + // The users that will actually be p-tagged: the chip list minus the muted ones. + fun activeNotifies(): List? = pTags?.filter { it.pubkeyHex !in mutedNotifies } + fun addToReplyList(user: User) { if (pTags?.contains(user) != true) { pTags = (pTags ?: emptyList()).plus(user) } + mutedNotifies = mutedNotifies - user.pubkeyHex } // A single ephemeral signer reused for the whole compose session so that media @@ -569,6 +588,7 @@ open class ShortNotePostViewModel : originalNote = replyingTo privateNoteLocked = replyingTo?.isPrivateRumor() == true wantsPrivateNote = privateNoteLocked + mutedNotifies = emptySet() replyingTo?.let { replyNote -> if (replyNote.event is BaseThreadedEvent) { this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote) @@ -577,20 +597,7 @@ open class ShortNotePostViewModel : } if (replyNote.event !is CommunityDefinitionEvent) { - replyNote.author?.let { replyUser -> - val currentMentions = - (replyNote.event as? TextNoteEvent) - ?.mentions() - ?.toSet() - ?.map { LocalCache.getOrCreateUser(it.pubKey) } - ?: emptyList() - - if (currentMentions.contains(replyUser)) { - this.pTags = currentMentions - } else { - this.pTags = currentMentions.plus(replyUser) - } - } + this.pTags = threadMembers(replyNote, this.eTags) } } ?: run { @@ -708,6 +715,22 @@ open class ShortNotePostViewModel : } } + // Everyone taking part in the thread gets a Notify chip: whoever the parent + // already p-tags, plus the author of every note in the reply chain. Members + // the user doesn't want to ping are muted via their chip's bell, not removed. + private fun threadMembers( + replyNote: Note, + threadNotes: List?, + ): List { + val mentions = + (replyNote.event as? TextNoteEvent) + ?.mentions() + ?.map { LocalCache.getOrCreateUser(it.pubKey) } + ?: emptyList() + val authors = threadNotes?.mapNotNull { it.author } ?: emptyList() + return (mentions + authors + listOfNotNull(replyNote.author)).distinct() + } + private fun loadFromDraft(draftEvent: TextNoteEvent) { canAddInvoice = accountViewModel.userProfile().lnAddress() != null canAddZapRaiser = accountViewModel.userProfile().lnAddress() != null @@ -786,6 +809,19 @@ open class ShortNotePostViewModel : privateNoteLocked = originalNote?.isPrivateRumor() == true wantsPrivateNote = privateNoteLocked + // A muted thread member is simply absent from the draft's p tags, so + // rebuild the full chip list and mark whoever the draft dropped as muted. + val draftNotifies = pTags.orEmpty().map { it.pubkeyHex }.toSet() + val members = + originalNote + ?.takeIf { it.event !is CommunityDefinitionEvent } + ?.let { threadMembers(it, eTags) } + .orEmpty() + mutedNotifies = members.mapNotNullTo(mutableSetOf()) { member -> member.pubkeyHex.takeIf { it !in draftNotifies } } + if (members.isNotEmpty()) { + pTags = (pTags.orEmpty() + members).distinct() + } + if (forwardZapTo.value.items.isNotEmpty()) { wantsForwardZapTo = true } @@ -845,6 +881,7 @@ open class ShortNotePostViewModel : draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { LocalCache.checkGetOrCreateUser(it[1]) } + mutedNotifies = emptySet() canUsePoll = originalNote == null canUseZapPoll = originalNote == null @@ -917,6 +954,7 @@ open class ShortNotePostViewModel : draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { LocalCache.checkGetOrCreateUser(it[1]) } + mutedNotifies = emptySet() canUsePoll = originalNote == null canUseZapPoll = originalNote == null @@ -1168,9 +1206,11 @@ open class ShortNotePostViewModel : val tags = prepareETagsAsReplyTo(replyingTo, null) accountViewModel.fixReplyTagHints(tags) markedETags(tags) - notify(replyingTo.toPTag()) + if (replyingTo.event.pubKey !in mutedNotifies) { + notify(replyingTo.toPTag()) + } } - pTags?.let { userList -> + activeNotifies()?.let { userList -> val tags = userList.map { val tag = it.toPTag() @@ -1190,7 +1230,7 @@ open class ShortNotePostViewModel : val tagger = NewMessageTagger( message.text.toString().trim(), - pTags, + activeNotifies(), eTags, accountViewModel, ) @@ -1512,6 +1552,7 @@ open class ShortNotePostViewModel : voiceSelectedServer = null voiceOrchestrator = null pTags = null + mutedNotifies = emptySet() wantsPoll = false pollOptions = newStateMapPollOptions() @@ -1562,10 +1603,6 @@ open class ShortNotePostViewModel : this.multiOrchestrator?.remove(selected) } - open fun removeFromReplyList(userToRemove: User) { - pTags = pTags?.filter { it != userToRemove } - } - open fun addToMessage(it: String) { message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it) onMessageChanged() diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 994f57d2a0..4f1f0685b0 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1240,6 +1240,8 @@ No receivers yet: only you will be able to see this note. Add people to share it with. Add Remove user from notifications + Notifying. Tap to mute the notification for this user + Muted. Tap to notify this user again Search and add a user to notify is not a bookmark here Remove bookmark from list diff --git a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf index 01bd58472eab53e9922f2ac64092cd463a60370c..07e1c3e472ddbf431f5352f9063062208b4f8b40 100644 GIT binary patch delta 5410 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 delta 2282 zcmYLJ2~d-WC*s=NP| zcGbEADl-TG7d|n_4C$G%^=zjXaJLR1wMkieQ_c431fW|5Q02^Yzd(K3ycnRZpGW1V zXXrCFKCRde^y#>MzM!&TDMmj066k-xbz@;^LHP=On;iICJRmu@ko#+%l{WMJZ@GVH zQNc2EIbX;)VKtWyi%M25+&(AoPn?_+p88-B4_Gg()&Tc)koct(FDNJqZ4Y2TYXjdO zQp^qZefDm=ptD>bTU=UEm2)t27x!NQ>>igaDJ}11|*L2>dbdRghzlGH6mzWKed{fuOI027-Rox@vv3aoYLX zTJ1^gy#R zZef$clENCoz6qCx8^f1}Z;l9wFxN+PMLds;kNhCAA@X6AEJ_=d9aR$b-Bi`okEV7+ zr$pCAw?_XQql{S`vsX{`{`xKYR{g8kfY`;cHL(npf;1s2 z;nRfg68#dFCN?HMH3S(7466)vhR+SB4PAyG4Z}&}lERWQk_wVmB;A}ge%cQ6v^UB6 z#Fj-Z9aE?Jx2jCFMb$LMOSMMDZ@lUahnuQc6{GT2 zMXAEfDt?JP`A#|Lu|R21DwQ6}qwcSj;mSOvw^DH5=U%MnRD`?# z0Z_Cmio{0og)Ks{UTjq8#BF?)X4A!MVwZSFtQON1TNP%7ox&ns5IwkkSd3GoD5B-J z6Q_$lfc%Cik-w8ak~i^ni73cViuUsN<=5q}<&)i3x(&LSOV@JG57Gxx z2dUBdjPq)#y>o~(S~|g*Gcl*1oQf>!3&$A#+JSg7Ac7kzxcOk}O0yNkP*HsPF^v#~qSt^}QHOX(eF!qHoA*{PrEOnl9~f zw90RO>tN}==SF5*Ufzo!n=FCdN8%phHGai^2qBb6h$C?#qlk=*Cay$I6h!%33!cQA zOyG_9k}1TW1lpP-IucIwB%UOaWXsqdPfKD?fMsRRSVvCFhl2l}_V*0gS?l{p**QKX zm9~c?X|Wy}7(vbMk3qfSLC0+X^?45JJ0EoNAgEsjXuwm@uoTef4$zomp!z+av715T zszBrCf+i$^CQbx3$Uu{L{EStgsdb>ny`X8|gQjzP2H%sp1~fYvbk<4G+1{XYn?Un9 zSCJNGy08JXm=l($K$mdm(k#$QPQHp`wGwnqCFojCww@>6@CoQfZr@}Ct$7Jr%L!}W zfo|yq{nP}ytp&7R4Z4Hp+}Q@YYXJ1mfuQ?&+`&Z9Bb}huD$wIR{_AGY&hci@%RI@I zVbFhZ|lfS-6lkGSpeRnT9!e7gb6u@lVM8EkYdm^c;8eE>||0Oq9x z^U;D$GJ*wf1Jj)Wi{*P0Zh=jo4VKyrX5z>=36_@&Ha8yZeRCyP*$A+uU0}-_!B+Hw zt;zu7owGH^!Pb_6ZJq+QwF2z3yvQ^8m2ka|e)HxH_MK##vd@%F%{a|+i*1Z|5Zxz^}1Z;>`_~tDHG7-Xv zHV7`m5ImY7XwE@+uM>iwGlWn(2w`OqVqZZ>`~*U3J%qF@2)W!ohue!QA(ZZhurdUK zSps2mIfS||AT$<1*gFNnUsE6)_!-&42@`}f`4HL%A)Ma|p-Tqg3b%dF6W=jH=;ehB z@MKTCAw1{4;c5u)UP59w2#KT_5}6Vb`Ef`*jF5OPgk)?zBtD<5tsB}{H$*t>PSp+3 UP0kzd3yeeHkes?N*k4or7afLy{r~^~ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt index a038c5603d..adee791893 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt @@ -164,6 +164,7 @@ object MaterialSymbols { val NoAccounts = MaterialSymbol("\uF03E") val NoEncryption = MaterialSymbol("\uF03F") val Notifications = MaterialSymbol("\uE7F5") + val NotificationsOff = MaterialSymbol("\uE7F6") val Numbers = MaterialSymbol("\uEAC7") val OpenInBrowser = MaterialSymbol("\uE89D") val OpenInFull = MaterialSymbol("\uF1CE")