WIP Markdown

This commit is contained in:
Philipp Heckel 2024-04-01 20:46:33 -04:00
parent 834e5aaec7
commit 51aed195aa
14 changed files with 460 additions and 18 deletions

View file

@ -36,6 +36,8 @@ android {
debug {
minifyEnabled false
debuggable true
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
}
}
@ -128,4 +130,14 @@ dependencies {
// Image viewer
implementation 'com.github.stfalcon-studio:StfalconImageViewer:v1.0.1'
// Better click handling for links
implementation 'me.saket:better-link-movement-method:2.2.0'
// Markdown
implementation 'io.noties.markwon:core:4.6.2'
implementation 'io.noties.markwon:image-picasso:4.6.2'
implementation 'io.noties.markwon:image:4.6.2'
implementation 'io.noties.markwon:ext-tables:4.6.2'
implementation 'io.noties.markwon:ext-strikethrough:4.6.2'
}

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "44fc291d937fdf02b9bc2d0abb10d2e0",
"identityHash": "208f16743f21d9c374f1314878eb93cb",
"entities": [
{
"tableName": "Subscription",
@ -94,10 +94,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
],
"autoGenerate": false
]
},
"indices": [
{
@ -124,7 +124,7 @@
},
{
"tableName": "Notification",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `icon_url` TEXT, `icon_contentUri` TEXT, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `contentType` TEXT NOT NULL, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `icon_url` TEXT, `icon_contentUri` TEXT, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
"fields": [
{
"fieldPath": "id",
@ -156,6 +156,12 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentType",
"columnName": "contentType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encoding",
"columnName": "encoding",
@ -255,11 +261,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"subscriptionId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -288,10 +294,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"baseUrl"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -338,10 +344,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
],
"autoGenerate": true
]
},
"indices": [],
"foreignKeys": []
@ -350,7 +356,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '44fc291d937fdf02b9bc2d0abb10d2e0')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '208f16743f21d9c374f1314878eb93cb')"
]
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">ntfy (debug)</string>
</resources>

View file

@ -183,6 +183,7 @@ class Backuper(val context: Context) {
timestamp = n.timestamp,
title = n.title,
message = n.message,
contentType = n.contentType,
encoding = n.encoding,
notificationId = 0,
priority = n.priority,
@ -312,6 +313,7 @@ class Backuper(val context: Context) {
timestamp = n.timestamp,
title = n.title,
message = n.message,
contentType = n.contentType,
encoding = n.encoding,
priority = n.priority,
tags = n.tags,
@ -386,6 +388,7 @@ data class Notification(
val timestamp: Long,
val title: String,
val message: String,
val contentType: String, // "" or "text/markdown" (empty assumes "text/plain")
val encoding: String, // "base64" or ""
val priority: Int, // 1=min, 3=default, 5=max
val tags: String,

View file

@ -99,6 +99,7 @@ data class Notification(
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "message") val message: String,
@ColumnInfo(name = "contentType") val contentType: String, // "" or "text/markdown" (empty assume text/plain)
@ColumnInfo(name = "encoding") val encoding: String, // "base64" or ""
@ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID
@ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max
@ -110,6 +111,10 @@ data class Notification(
@ColumnInfo(name = "deleted") val deleted: Boolean,
)
fun Notification.isMarkdown(): Boolean {
return contentType == "text/markdown"
}
@Entity
data class Attachment(
@ColumnInfo(name = "name") val name: String, // Filename

View file

@ -28,6 +28,7 @@ class BroadcastService(private val ctx: Context) {
intent.putExtra("message", decodeMessage(notification))
intent.putExtra("message_bytes", decodeBytesMessage(notification))
intent.putExtra("message_encoding", notification.encoding)
intent.putExtra("content_type", notification.contentType)
intent.putExtra("tags", notification.tags)
intent.putExtra("tags_map", joinTagsMap(splitTags(notification.tags)))
intent.putExtra("priority", notification.priority)

View file

@ -1,6 +1,7 @@
package io.heckel.ntfy.msg
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
/* This annotation ensures that proguard still works in production builds,
* see https://stackoverflow.com/a/62753300/1440785 */
@ -17,6 +18,7 @@ data class Message(
val actions: List<MessageAction>?,
val title: String?,
val message: String,
@SerializedName("content_type") val contentType: String?,
val encoding: String?,
val attachment: MessageAttachment?,
)

View file

@ -57,6 +57,7 @@ class NotificationParser {
timestamp = message.time,
title = message.title ?: "",
message = message.message,
contentType = message.contentType ?: "",
encoding = message.encoding ?: "",
priority = toPriority(message.priority),
tags = joinTags(message.tags),

View file

@ -11,6 +11,8 @@ import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.SpannedString
import android.text.style.CharacterStyle
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
@ -26,6 +28,7 @@ import java.util.*
class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val repository = Repository.getInstance(context)
private val markwon = MarkwonFactory.createForNotification(context)
fun display(subscription: Subscription, notification: Notification) {
Log.d(TAG, "Displaying notification $notification")
@ -147,7 +150,7 @@ class NotificationService(val context: Context) {
try {
val attachmentBitmap = contentUri.readBitmapFromUri(context)
builder
.setContentText(maybeAppendActionErrors(formatMessage(notification), notification))
.setContentText(maybeAppendActionErrors(maybeMarkdown(formatMessage(notification), notification), notification))
.setLargeIcon(attachmentBitmap)
.setStyle(NotificationCompat.BigPictureStyle()
.bigPicture(attachmentBitmap)
@ -167,8 +170,8 @@ class NotificationService(val context: Context) {
}
}
private fun formatMessageMaybeWithAttachmentInfos(notification: Notification): String {
val message = formatMessage(notification)
private fun formatMessageMaybeWithAttachmentInfos(notification: Notification): CharSequence {
val message = maybeMarkdown(formatMessage(notification), notification)
val attachment = notification.attachment ?: return message
val attachmentInfos = if (attachment.size != null) {
"${attachment.name}, ${formatBytes(attachment.size)}"
@ -514,6 +517,13 @@ class NotificationService(val context: Context) {
}
}
private fun maybeMarkdown(message: String, notification: Notification): CharSequence {
if (notification.contentType == "text/markdown") {
return markwon.toMarkdown(message)
}
return message
}
companion object {
const val ACTION_VIEW = "view"
const val ACTION_HTTP = "http"

View file

@ -9,6 +9,8 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -33,20 +35,23 @@ import io.heckel.ntfy.msg.DownloadType
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
import io.heckel.ntfy.util.*
import io.noties.markwon.Markwon
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import me.saket.bettermovementmethod.BetterLinkMovementMethod
class DetailAdapter(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
private val markwon: Markwon = MarkwonFactory.createForMessage(activity)
val selected = mutableSetOf<String>() // Notification IDs
/* Creates and inflates view and return TopicViewHolder. */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_detail_item, parent, false)
return DetailViewHolder(activity, lifecycleScope, repository, view, selected, onClick, onLongClick)
return DetailViewHolder(activity, lifecycleScope, repository, markwon, view, selected, onClick, onLongClick)
}
/* Gets current topic and uses it to bind view. */
@ -73,7 +78,16 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
}
/* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */
class DetailViewHolder(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, itemView: View, private val selected: Set<String>, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) :
class DetailViewHolder(
private val activity: Activity,
private val lifecycleScope: CoroutineScope,
private val repository: Repository,
private val markwon: Markwon,
itemView: View,
private val selected: Set<String>,
val onClick: (Notification) -> Unit,
val onLongClick: (Notification) -> Unit
) :
RecyclerView.ViewHolder(itemView) {
private var notification: Notification? = null
private val layout: View = itemView.findViewById(R.id.detail_item_layout)
@ -100,7 +114,9 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
val unmatchedTags = unmatchedTags(splitTags(notification.tags))
dateView.text = formatDateShort(notification.timestamp)
messageView.text = maybeAppendActionErrors(formatMessage(notification), notification)
messageView.text = maybeAppendActionErrors(maybeMarkdown(formatMessage(notification), notification), notification)
messageView.autoLinkMask = if (notification.isMarkdown()) 0 else Linkify.WEB_URLS
messageView.movementMethod = BetterLinkMovementMethod.getInstance()
messageView.setOnClickListener {
// Click & Long-click listeners on the text as well, because "autoLink=web" makes them
// clickable, and so we cannot rely on the underlying card to perform the action.
@ -143,6 +159,13 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
maybeRenderActions(context, notification)
}
private fun maybeMarkdown(message: String, notification: Notification): CharSequence {
if (notification.isMarkdown()) {
return markwon.toMarkdown(message)
}
return message
}
private fun renderPriority(context: Context, notification: Notification) {
when (notification.priority) {
PRIORITY_MIN -> {

View file

@ -0,0 +1,108 @@
package io.heckel.ntfy.util
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.method.LinkMovementMethod
import android.text.style.*
import androidx.core.content.ContextCompat
import io.heckel.ntfy.R
import io.noties.markwon.*
import io.noties.markwon.core.CorePlugin
import io.noties.markwon.core.CoreProps
import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TableAwareMovementMethod
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tables.TableTheme
import io.noties.markwon.movement.MovementMethodPlugin
import me.saket.bettermovementmethod.BetterLinkMovementMethod
import org.commonmark.ext.gfm.tables.TableCell
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.node.*
import org.commonmark.parser.Parser
internal object MarkwonFactory {
fun createForMessage(context: Context): Markwon {
val headingSizes = floatArrayOf(1.7f, 1.5f, 1.2f, 1f, .8f, .7f)
val bulletGapWidth = (8 * context.resources.displayMetrics.density + 0.5f).toInt()
return Markwon.builder(context)
.usePlugin(CorePlugin.create())
.usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.getInstance()))
// .usePlugin(PicassoImagesPlugin.create(picasso))
.usePlugin(StrikethroughPlugin.create())
//.usePlugin(TablePlugin.create(context))
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureTheme(builder: MarkwonTheme.Builder) {
builder.linkColor(ContextCompat.getColor(context, R.color.teal))
.isLinkUnderlined(true)
}
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder
.setFactory(Heading::class.java) { _, props: RenderProps? ->
arrayOf<Any>(
RelativeSizeSpan(headingSizes[CoreProps.HEADING_LEVEL.require(props!!) - 1]),
StyleSpan(Typeface.BOLD)
)
}
.setFactory(Emphasis::class.java) { _, _ -> StyleSpan(Typeface.ITALIC) }
.setFactory(StrongEmphasis::class.java) { _, _ -> StyleSpan(Typeface.BOLD) }
.setFactory(BlockQuote::class.java) { _, _ -> QuoteSpan() }
.setFactory(Code::class.java) { _, _ ->
arrayOf<Any>(
BackgroundColorSpan(Color.LTGRAY),
TypefaceSpan("monospace")
)
}
.setFactory(ListItem::class.java) { _, _ -> BulletSpan(bulletGapWidth) }
}
})
.build()
}
fun createForNotification(context: Context): Markwon {
val headingSizes = floatArrayOf(2f, 1.5f, 1.17f, 1f, .83f, .67f)
val bulletGapWidth = (8 * context.resources.displayMetrics.density + 0.5f).toInt()
return Markwon.builder(context)
.usePlugin(CorePlugin.create())
//.usePlugin(PicassoImagesPlugin.create(picasso))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder
.setFactory(Heading::class.java) { _, props: RenderProps? ->
arrayOf<Any>(
RelativeSizeSpan(headingSizes[CoreProps.HEADING_LEVEL.require(props!!) - 1]),
StyleSpan(Typeface.BOLD)
)
}
.setFactory(Emphasis::class.java) { _, _ -> StyleSpan(Typeface.ITALIC) }
.setFactory(StrongEmphasis::class.java) { _, _ -> StyleSpan(Typeface.BOLD) }
.setFactory(BlockQuote::class.java) { _, _ -> QuoteSpan() }
.setFactory(Code::class.java) { _, _ ->
arrayOf<Any>(
BackgroundColorSpan(Color.LTGRAY),
TypefaceSpan("monospace")
)
}
.setFactory(ListItem::class.java) { _, _ -> BulletSpan(bulletGapWidth) }
.setFactory(Link::class.java) { _, _ -> null }
}
override fun configureParser(builder: Parser.Builder) {
builder.extensions(setOf(TablesExtension.create()))
}
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder.on(TableCell::class.java) { visitor: MarkwonVisitor, node: TableCell? ->
visitor.visitChildren(node!!)
visitor.builder().append(' ')
}
}
})
.build()
}
}

View file

@ -212,7 +212,7 @@ fun formatActionLabel(action: Action): String {
}
}
fun maybeAppendActionErrors(message: String, notification: Notification): String {
fun maybeAppendActionErrors(message: CharSequence, notification: Notification): CharSequence {
val actionErrors = notification.actions
.orEmpty()
.mapNotNull { action -> action.error }

View file

@ -92,6 +92,7 @@ class FirebaseService : FirebaseMessagingService() {
val click = data["click"]
val iconUrl = data["icon"]
val actions = data["actions"] // JSON array as string, sigh ...
val contentType = data["content_type"]
val encoding = data["encoding"]
val attachmentName = data["attachment_name"] ?: "attachment.bin"
val attachmentType = data["attachment_type"]
@ -132,6 +133,7 @@ class FirebaseService : FirebaseMessagingService() {
timestamp = timestamp,
title = title ?: "",
message = message,
contentType = contentType ?: "",
encoding = encoding ?: "",
priority = toPriority(priority),
tags = tags ?: "",

265
assets/logo_with_text.svg Normal file
View file

@ -0,0 +1,265 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="50mm"
height="50mm"
viewBox="0 0 50 50"
version="1.1"
id="svg8"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="logo_with_text.svg"
inkscape:export-filename="/home/pheckel/Code/ntfy-android/assets/appstore_ios.png"
inkscape:export-xdpi="520.19202"
inkscape:export-ydpi="520.19202"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient4714">
<stop
style="stop-color:#348878;stop-opacity:1"
offset="0"
id="stop4710" />
<stop
style="stop-color:#52bca6;stop-opacity:1"
offset="1"
id="stop4712" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient28858-5">
<stop
style="stop-color:#348878;stop-opacity:1"
offset="0"
id="stop28854-3" />
<stop
style="stop-color:#56bda8;stop-opacity:1"
offset="1"
id="stop28856-5" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient28858-5"
id="linearGradient3255"
x1="160.72209"
y1="128.53317"
x2="168.41153"
y2="134.32626"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.741513,0,0,1.741513,-224.24539,-136.36679)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4714"
id="linearGradient4633"
x1="0.034492966"
y1="-0.0003150744"
x2="50.319355"
y2="50.284546"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.46182346,0,0,0.46182346,-0.01592966,-1.6956565e-4)" />
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter3958"
x="-0.076083149"
y="-0.091641662"
width="1.1759423"
height="1.2114791">
<feFlood
flood-opacity="0.192157"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood3948" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite3950" />
<feGaussianBlur
in="composite1"
stdDeviation="4"
result="blur"
id="feGaussianBlur3952" />
<feOffset
dx="3"
dy="2.95367"
result="offset"
id="feOffset3954" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite3956" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.4713302"
inkscape:cx="74.139011"
inkscape:cy="89.123366"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:snap-text-baseline="true"
inkscape:window-width="1846"
inkscape:window-height="1016"
inkscape:window-x="74"
inkscape:window-y="27"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="false"
inkscape:guide-bbox="true"
inkscape:pagecheckerboard="0">
<sodipodi:guide
position="10.173514,67.718331"
orientation="1,0"
id="guide1770" />
<sodipodi:guide
position="39.965574,62.077508"
orientation="1,0"
id="guide1772" />
<sodipodi:guide
position="10.173514,39.789015"
orientation="0,-1"
id="guide1774" />
<sodipodi:guide
position="-2.3077334,9.9462015"
orientation="0,-1"
id="guide1776" />
<sodipodi:guide
position="14.990626,36.198285"
orientation="1,0"
id="guide4020" />
<sodipodi:guide
position="34.930725,39.789015"
orientation="1,0"
id="guide4022" />
<sodipodi:guide
position="12.7026,32.00465"
orientation="0,-1"
id="guide4024" />
<sodipodi:guide
position="11.377711,17.981227"
orientation="0,-1"
id="guide4026" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="background"
style="display:inline">
<rect
style="fill:url(#linearGradient4633);fill-opacity:1;stroke:none;stroke-width:0.133067;stroke-linejoin:bevel"
id="rect4545"
width="23.222729"
height="23.222729"
x="0"
y="-0.0003150744" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="drop shadow"
style="display:inline">
<path
id="path3646"
style="color:#000000;display:inline;fill:#ffffff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none;filter:url(#filter3958)"
d="m 50.400391,46.882812 c -9.16879,0 -17.023438,7.2146 -17.023438,16.386719 v 0.0078 l 0.08984,71.369139 -2.302735,16.99219 31.3125,-8.31836 h 77.841802 c 9.16877,0 17.02344,-7.22425 17.02344,-16.39648 V 63.269531 c 0,-9.169496 -7.85031,-16.382463 -17.01563,-16.386719 h -0.008 z m 0,11.566407 h 89.917969 0.008 c 3.22151,0.0033 5.44922,2.346918 5.44922,4.820312 v 63.654299 c 0,2.47551 -2.23164,4.82031 -5.45703,4.82031 H 60.779297 l -15.908203,4.80664 0.162109,-0.9375 -0.08789,-72.343749 c 0,-2.475337 2.229739,-4.820312 5.455078,-4.820312 z"
transform="matrix(0.12288694,0,0,0.12288694,0,-1.6873665e-4)" />
</g>
<g
inkscape:label="foreground"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-51.147327,-81.515579)"
style="display:inline">
<path
style="color:#000000;fill:url(#linearGradient3255);stroke:none;stroke-width:1.15908;-inkscape-stroke:none"
d="M 68.356939,87.921737 H 57.543468 c -0.743432,0 -1.351677,0.574055 -1.351677,1.275675 l 0.01033,8.639597 -0.201904,1.163784 2.894292,-0.873723 h 9.46176 c 0.743433,0 1.351694,-0.574052 1.351694,-1.275689 v -7.653969 c 0,-0.70162 -0.608261,-1.275675 -1.351694,-1.275675 z"
id="path7368" />
<path
id="path2498"
style="color:#000000;fill:#ffffff;stroke:none;stroke-width:0.237311;-inkscape-stroke:none"
d="m 57.340877,87.276696 c -1.126725,0 -2.091958,0.88658 -2.091958,2.013713 v 9.59e-4 l 0.01104,8.770335 -0.282976,2.088117 3.847897,-1.022216 h 9.565741 c 1.126722,0 2.091958,-0.887766 2.091958,-2.014914 v -7.822281 c 0,-1.126811 -0.9647,-2.01319 -2.090998,-2.013713 h -9.83e-4 z m 0,1.42136 h 11.049744 9.83e-4 c 0.395882,4.06e-4 0.669638,0.288406 0.669638,0.592353 v 7.822282 c 0,0.304208 -0.274239,0.592354 -0.670598,0.592354 h -9.774335 l -1.954911,0.590673 0.01992,-0.115207 -0.0108,-8.890102 c 0,-0.304186 0.274006,-0.592353 0.670359,-0.592353 z" />
<g
id="path1011-6-2"
transform="matrix(0.67515919,0,0,0.80264534,16.256543,1.6511734)"
style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke:none;stroke-width:0.525121">
<path
style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m 62.57046,116.77004 v -1.31201 l 3.280018,-1.45904 q 0.158346,-0.0679 0.305381,-0.1018 0.158346,-0.0452 0.282761,-0.0679 0.135725,-0.0113 0.271449,-0.0226 v -0.0905 q -0.135724,-0.0113 -0.271449,-0.0452 -0.124415,-0.0226 -0.282761,-0.0566 -0.147035,-0.0452 -0.305381,-0.1131 l -3.280018,-1.45904 v -1.32332 l 5.067063,2.31863 v 1.4138 z"
id="path7553" />
<path
style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m 62.308594,110.31055 v 1.90234 l 3.4375,1.5293 c 0.0073,0.003 0.0142,0.005 0.02148,0.008 -0.0073,0.003 -0.0142,0.005 -0.02148,0.008 l -3.4375,1.5293 v 1.89258 l 0.371093,-0.16992 5.220704,-2.39063 v -1.75 z m 0.52539,0.8164 4.541016,2.08008 v 1.07617 l -4.541016,2.07813 v -0.73242 l 3.119141,-1.38868 0.0039,-0.002 c 0.09141,-0.0389 0.178343,-0.0676 0.257813,-0.0859 h 0.0059 l 0.0078,-0.002 c 0.09483,-0.0271 0.176055,-0.0474 0.246093,-0.0606 l 0.498047,-0.041 v -0.57422 l -0.240234,-0.0195 c -0.07606,-0.006 -0.153294,-0.0198 -0.230469,-0.0391 l -0.0078,-0.002 -0.0078,-0.002 c -0.07608,-0.0138 -0.16556,-0.0318 -0.263672,-0.0527 -0.08398,-0.0262 -0.172736,-0.058 -0.265625,-0.0977 l -0.0039,-0.002 -3.119141,-1.38868 z"
id="path7555" />
</g>
<g
id="g1224"
transform="matrix(0.67315851,0,0,0.77291871,16.72925,3.9953612)"
style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke:none;stroke-width:0.525121">
<path
style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m 69.17132,117.75404 h 5.428996 v 1.27808 H 69.17132 Z"
id="path1220" />
<path
style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m 68.908203,117.49219 v 0.26172 1.54101 h 5.955078 v -1.80273 z m 0.525391,0.52344 h 4.904297 v 0.7539 h -4.904297 z"
id="path1222" />
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="75.847778"
y="94.99572"
id="text7539"><tspan
sodipodi:role="line"
id="tspan7537"
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-family:Inter;-inkscape-font-specification:'Inter Heavy';stroke-width:0.264583"
x="75.847778"
y="94.99572">ntfy.sh</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="round icon preview"
style="display:none">
<path
id="path18850-8-1"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.255654"
d="M 50.337488,80.973198 V 131.61213 H 101.65302 V 80.973198 Z m 25.676545,1.442307 h 0.555989 a 24.369387,24.369387 0 0 1 23.860308,21.232925 v 6.09963 a 24.369387,24.369387 0 0 1 -21.288308,21.19336 h 21.288308 v 0.0138 H 51.963792 v -0.0158 H 73.428179 A 24.369387,24.369387 0 0 1 51.963792,107.97535 v -2.49089 A 24.369387,24.369387 0 0 1 76.014033,82.415508 Z"
transform="translate(-51.147326,-81.51558)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB