Support for tags and emojis

This commit is contained in:
Philipp Heckel 2022-05-25 11:26:23 -04:00
parent ec6472d47d
commit 6d40c441f9
53 changed files with 22915 additions and 27 deletions

View file

@ -34,6 +34,10 @@
94A3F7C8283734D900C48E79 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */; };
94A3F7CA28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; };
94A3F7CB28386B2100C48E79 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A3F7C928386B2100C48E79 /* Config.swift */; };
94CD1966283E662900973B93 /* emojis.json in Resources */ = {isa = PBXBuildFile; fileRef = 94CD1965283E662900973B93 /* emojis.json */; };
94CD1967283E662900973B93 /* emojis.json in Resources */ = {isa = PBXBuildFile; fileRef = 94CD1965283E662900973B93 /* emojis.json */; };
94CD196A283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; };
94CD196B283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; };
94E9196C28353E0100F30170 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; };
/* End PBXBuildFile section */
@ -69,7 +73,7 @@
9474F1C7282F2AA800CDE4DD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
9474F1D1282F2D2C00CDE4DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9474F1D5282F2FED00CDE4DD /* ntfy.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ntfy.entitlements; sourceTree = "<group>"; };
9474F1D6282F2FF700CDE4DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
9474F1D6282F2FF700CDE4DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9474F1DB282F30B500CDE4DD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
9474F1E4282F3FFD00CDE4DD /* ntfyNSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ntfyNSE.appex; sourceTree = BUILT_PRODUCTS_DIR; };
9474F1E6282F3FFD00CDE4DD /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
@ -87,6 +91,8 @@
9474F216283531A200CDE4DD /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
94A3F7C928386B2100C48E79 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = "<group>"; };
94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -130,14 +136,13 @@
9474F1BF282F2AA700CDE4DD /* ntfy */ = {
isa = PBXGroup;
children = (
9474F210283326E000CDE4DD /* Utils */,
9474F20D2833264F00CDE4DD /* App */,
9474F1C4282F2AA800CDE4DD /* Assets.xcassets */,
9474F1DB282F30B500CDE4DD /* GoogleService-Info.plist */,
9474F1D6282F2FF700CDE4DD /* Info.plist */,
9474F1D5282F2FED00CDE4DD /* ntfy.entitlements */,
9474F1D6282F2FF700CDE4DD /* Info.plist */,
9474F20D2833264F00CDE4DD /* App */,
94CD1968283E663700973B93 /* Assets */,
9474F2032831725A00CDE4DD /* Persistence */,
9474F1C6282F2AA800CDE4DD /* Preview Content */,
9474F210283326E000CDE4DD /* Utils */,
9474F202283170F000CDE4DD /* Views */,
);
path = ntfy;
@ -204,6 +209,7 @@
isa = PBXGroup;
children = (
9474F20E283326C500CDE4DD /* ApiService.swift */,
94CD1969283E666900973B93 /* EmojiManager.swift */,
9474F211283327C200CDE4DD /* Helpers.swift */,
94A3F7C928386B2100C48E79 /* Config.swift */,
9474F216283531A200CDE4DD /* Log.swift */,
@ -211,6 +217,16 @@
path = Utils;
sourceTree = "<group>";
};
94CD1968283E663700973B93 /* Assets */ = {
isa = PBXGroup;
children = (
9474F1C4282F2AA800CDE4DD /* Assets.xcassets */,
94CD1965283E662900973B93 /* emojis.json */,
9474F1DB282F30B500CDE4DD /* GoogleService-Info.plist */,
);
path = Assets;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -300,6 +316,7 @@
files = (
9474F1C8282F2AA800CDE4DD /* Preview Assets.xcassets in Resources */,
9474F1C5282F2AA800CDE4DD /* Assets.xcassets in Resources */,
94CD1966283E662900973B93 /* emojis.json in Resources */,
9474F1DC282F30B500CDE4DD /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -308,6 +325,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
94CD1967283E662900973B93 /* emojis.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -330,6 +348,7 @@
9474F1F22830825600CDE4DD /* SubscriptionListView.swift in Sources */,
9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */,
9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */,
94CD196A283E666900973B93 /* EmojiManager.swift in Sources */,
9474F1C1282F2AA700CDE4DD /* AppMain.swift in Sources */,
9474F20F283326C500CDE4DD /* ApiService.swift in Sources */,
9474F1F72830830700CDE4DD /* ntfy.xcdatamodeld in Sources */,
@ -344,6 +363,7 @@
94E9196C28353E0100F30170 /* Log.swift in Sources */,
9474F2152834758700CDE4DD /* Helpers.swift in Sources */,
9474F1E7282F3FFD00CDE4DD /* NotificationService.swift in Sources */,
94CD196B283E666900973B93 /* EmojiManager.swift in Sources */,
9474F2052831D51500CDE4DD /* Store.swift in Sources */,
9474F2062831D73C00CDE4DD /* ntfy.xcdatamodeld in Sources */,
94A3F7CB28386B2100C48E79 /* Config.swift in Sources */,

View file

@ -30,8 +30,8 @@
filePath = "ntfyNSE/NotificationService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "58"
endingLineNumber = "58"
startingLineNumber = "70"
endingLineNumber = "70"
landmarkName = "didReceive(_:withContentHandler:)"
landmarkType = "7">
</BreakpointContent>
@ -46,8 +46,8 @@
filePath = "ntfyNSE/NotificationService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "26"
endingLineNumber = "26"
startingLineNumber = "32"
endingLineNumber = "32"
landmarkName = "didReceive(_:withContentHandler:)"
landmarkType = "7">
</BreakpointContent>
@ -62,8 +62,8 @@
filePath = "ntfyNSE/NotificationService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "29"
endingLineNumber = "29"
startingLineNumber = "33"
endingLineNumber = "33"
landmarkName = "didReceive(_:withContentHandler:)"
landmarkType = "7">
</BreakpointContent>
@ -78,8 +78,8 @@
filePath = "ntfyNSE/NotificationService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "57"
endingLineNumber = "57"
startingLineNumber = "69"
endingLineNumber = "69"
landmarkName = "didReceive(_:withContentHandler:)"
landmarkType = "7">
</BreakpointContent>

View file

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 198 KiB

View file

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View file

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 729 B

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 985 B

After

Width:  |  Height:  |  Size: 985 B

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 833 B

View file

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 605 B

View file

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 847 B

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

22747
ntfy/Assets/emojis.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,41 @@ extension Notification {
return dateFormatter.string(from: date)
}
func formatMessage() -> String {
let message = message ?? ""
if let title = title, title != "" {
return message
}
let emojiTags = emojiTags()
if !emojiTags.isEmpty {
return emojiTags.joined(separator: "") + " " + message
}
return message
}
func formatTitle() -> String? {
if let title = title, title != "" {
let emojiTags = emojiTags()
if !emojiTags.isEmpty {
return emojiTags.joined(separator: "") + " " + title
}
return title
}
return nil
}
func allTags() -> [String] {
return parseAllTags(tags)
}
func emojiTags() -> [String] {
return parseEmojiTags(tags)
}
func nonEmojiTags() -> [String] {
return parseNonEmojiTags(tags)
}
}
/// This is the "on the wire" message as it is received from the ntfy server
@ -32,4 +67,5 @@ struct Message: Decodable {
var message: String?
var title: String?
var priority: Int16?
var tags: [String]?
}

View file

@ -89,12 +89,16 @@ class Store: ObservableObject {
Log.d(Store.tag, "Subscription for topic \(topic) unknown")
return
}
let title = userInfo["title"] as? String ?? ""
let priority = Int16(userInfo["priority"] as? String ?? "3") ?? 3
let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",")
let m = Message(
id: id,
time: timeInt,
message: message,
title: userInfo["title"] as? String ?? "",
priority: Int16(userInfo["priority"] as? String ?? "3") ?? 3
title: title,
priority: priority,
tags: tags
)
save(notificationFromMessage: m, withSubscription: subscription)
}
@ -107,6 +111,7 @@ class Store: ObservableObject {
notification.message = message.message ?? ""
notification.title = message.title ?? ""
notification.priority = (message.priority != nil && message.priority != 0) ? message.priority! : 3
notification.tags = message.tags?.joined(separator: ",") ?? ""
subscription.addToNotifications(notification)
subscription.lastNotificationId = message.id
try context.save()
@ -172,9 +177,9 @@ class Store: ObservableObject {
extension Store {
static let sampleData = [
"stats": [
Message(id: "1", time: 1653048956, message: "In the last 24 hours, hyou had 5,000 users across 13 countries visit your website", title: "Record visitor numbers", priority: 4),
Message(id: "2", time: 1653058956, message: "201 users/h\n80 IPs", title: "This is a title", priority: 1),
Message(id: "3", time: 1643058956, message: "This message does not have a title, but is instead super long. Like really really long. It can't be any longer I think. I mean, there is s 4,000 byte limit of the message, so I guess I have to make this 4,000 bytes long. Or do I? 😁 I don't know. It's quite tedious to come up with something so long, so I'll stop now. Bye!", title: nil, priority: 5)
Message(id: "1", time: 1653048956, message: "In the last 24 hours, hyou had 5,000 users across 13 countries visit your website", title: "Record visitor numbers", priority: 4, tags: ["smile", "server123", "de"]),
Message(id: "2", time: 1653058956, message: "201 users/h\n80 IPs", title: "This is a title", priority: 1, tags: []),
Message(id: "3", time: 1643058956, message: "This message does not have a title, but is instead super long. Like really really long. It can't be any longer I think. I mean, there is s 4,000 byte limit of the message, so I guess I have to make this 4,000 bytes long. Or do I? 😁 I don't know. It's quite tedious to come up with something so long, so I'll stop now. Bye!", title: nil, priority: 5, tags: ["facepalm"])
],
"backups": [],
"announcements": [],
@ -214,6 +219,7 @@ extension Store {
notification.message = message.message
notification.title = message.title
notification.priority = message.priority ?? 3
notification.tags = message.tags?.joined(separator: ",") ?? ""
return notification
}
}

View file

@ -0,0 +1,42 @@
import Foundation
struct Emoji: Decodable {
let emoji: String
let aliases: [String]
let tags: [String]
func getUnicode() -> String {
return emoji
}
}
class EmojiManager {
private static let tag = "EmojiManager"
private static var emojis: Dictionary<String, Emoji> = [:]
static let shared = EmojiManager()
init() {
// emojis.json pulled from https://github.com/github/gemoji/blob/master/db/emoji.json
if let url = Bundle.main.url(forResource: "emojis", withExtension: "json") {
do {
let jsonData = try Data(contentsOf: url)
if let jsonEmojis = try? JSONDecoder().decode([Emoji].self, from: jsonData) {
for emoji in jsonEmojis {
if !emoji.aliases.isEmpty {
EmojiManager.emojis[emoji.aliases.first!] = emoji
}
}
}
} catch {
Log.e(EmojiManager.tag, "Unable to load emojis: \(error.localizedDescription)", error)
}
}
}
func getEmojiByAlias(alias: String) -> Emoji? {
if alias.isEmpty {
return nil
}
return EmojiManager.emojis[alias]
}
}

View file

@ -9,3 +9,23 @@ func topicShortUrl(baseUrl: String, topic: String) -> String {
.replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "https://", with: "")
}
func parseAllTags(_ tags: String?) -> [String] {
return (tags?.components(separatedBy: ",") ?? [])
.filter { $0.trimmingCharacters(in: [" "]) != "" }
}
func parseEmojiTags(_ tags: String?) -> [String] {
var emojiTags: [String] = []
for tag in parseAllTags(tags) {
if let emoji = EmojiManager.shared.getEmojiByAlias(alias: tag) {
emojiTags.append(emoji.getUnicode())
}
}
return emojiTags
}
func parseNonEmojiTags(_ tags: String?) -> [String] {
return parseAllTags(tags)
.filter { EmojiManager.shared.getEmojiByAlias(alias: $0) == nil }
}

View file

@ -234,13 +234,18 @@ struct NotificationRowView: View {
.frame(width: 16, height: 16)
}
}
if let title = notification.title, title != "" {
if let title = notification.formatTitle(), title != "" {
Text(title)
.font(.headline)
.bold()
}
Text(notification.message ?? "")
Text(notification.formatMessage())
.font(.body)
if !notification.nonEmojiTags().isEmpty {
Text("Tags: " + notification.nonEmojiTags().joined(separator: ", "))
.font(.subheadline)
.foregroundColor(.gray)
}
}
.padding(.all, 4)
.swipeActions(edge: .trailing) {

View file

@ -21,21 +21,33 @@ class NotificationService: UNNotificationServiceExtension {
if let bestAttemptContent = bestAttemptContent {
let userInfo = bestAttemptContent.userInfo
// Get all the things
let topic = userInfo["topic"] as? String ?? ""
let title = userInfo["title"] as? String
let priority = userInfo["priority"] as? String ?? "3"
let tags = userInfo["tags"] as? String
// Set notification title to short URL if there is no title. The title is always set
// by the server, but it may be empty.
if let topic = userInfo["topic"] as? String,
let title = userInfo["title"] as? String {
if title == "" {
bestAttemptContent.title = topicShortUrl(baseUrl: Config.appBaseUrl, topic: topic)
if let title = title, title == "" {
bestAttemptContent.title = topicShortUrl(baseUrl: Config.appBaseUrl, topic: topic)
}
// Emojify title or message
let emojiTags = parseEmojiTags(tags)
if !emojiTags.isEmpty {
if let title = title, title != "" {
bestAttemptContent.title = emojiTags.joined(separator: "") + " " + bestAttemptContent.title
} else {
bestAttemptContent.body = emojiTags.joined(separator: "") + " " + bestAttemptContent.body
}
}
// Play a sound, and group by topic
bestAttemptContent.sound = .default
bestAttemptContent.threadIdentifier = userInfo["topic"] as? String ?? ""
bestAttemptContent.threadIdentifier = topic
// Map priorities to interruption level (light up screen, ...) and relevance (order)
let priority = userInfo["priority"] as? String ?? "3"
switch priority {
case "1":
bestAttemptContent.interruptionLevel = .passive