Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for mark-read-generic and mark-read-messages #4443

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7dad1c2
add support for `mark-read-generic` and `mark-read-messages`
haileyok Jun 8, 2024
4580054
nit
haileyok Jun 10, 2024
f195fad
implement changes
haileyok Jun 10, 2024
a67a17a
Merge remote-tracking branch 'origin/main' into hailey/decrement-badge
haileyok Jun 26, 2024
be738ca
store convo ids for incrementing dms
haileyok Jun 26, 2024
0e951e6
shared preferences api
haileyok Jun 26, 2024
c1ac691
rm log
haileyok Jun 26, 2024
e97bf27
tweak test
haileyok Jun 26, 2024
3ee7b50
Merge branch 'hailey/shared-preferences' into hailey/decrement-badge
haileyok Jun 26, 2024
86b2bca
start tweaks
haileyok Jun 26, 2024
1a8a154
shared instance of prefs for other modules
haileyok Jun 26, 2024
124e8f1
Merge branch 'hailey/shared-preferences' into hailey/decrement-badge
haileyok Jun 26, 2024
f24a4fe
public
haileyok Jun 26, 2024
fb12def
add any value set call
haileyok Jun 26, 2024
bf92de3
ios badge count updating
haileyok Jun 26, 2024
37ff335
shared preferences api
haileyok Jun 26, 2024
83056bb
Merge branch 'hailey/shared-preferences' into hailey/decrement-badge
haileyok Jun 26, 2024
d6a2ee4
JS changes
haileyok Jun 26, 2024
f9c5c42
load preferences
haileyok Jun 26, 2024
035fad8
implement js changes
haileyok Jun 26, 2024
ac0ba3a
add a few more helpers
haileyok Jun 26, 2024
f352320
merge in some changes
haileyok Jun 26, 2024
d976ab3
finish android impl
haileyok Jun 26, 2024
1948495
lint
haileyok Jun 26, 2024
6823d10
shared preferences api
haileyok Jun 26, 2024
82e53d6
Merge branch 'hailey/shared-preferences' into hailey/decrement-badge
haileyok Jun 26, 2024
ffe15a9
shared preferences api
haileyok Jun 26, 2024
cec1c3e
Merge branch 'hailey/shared-preferences' into hailey/decrement-badge
haileyok Jul 2, 2024
516c9e2
convert some things
haileyok Jul 2, 2024
849b5ab
Merge branch 'main' into hailey/decrement-badge
haileyok Jul 12, 2024
62aa42f
lint android
haileyok Jul 12, 2024
8e44de0
tweak
haileyok Jul 12, 2024
f5d231f
add functions to android
haileyok Jul 13, 2024
1e17849
update js
haileyok Jul 13, 2024
897e6d8
Merge branch 'main' into hailey/decrement-badge
haileyok Oct 4, 2024
14a975c
update nse
haileyok Oct 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 129 additions & 40 deletions modules/BlueskyNSE/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ import UIKit
let APP_GROUP = "group.app.bsky"
typealias ContentHandler = (UNNotificationContent) -> Void

enum NotificationType: String, CaseIterable {
case like
case repost
case follow
case reply
case quote
case chatMessage = "chat-message"
case markReadGeneric = "mark-read-generic"
case markReadMessages = "mark-read-messages"
case starterPackJoined = "starterpack-joined"
}

enum BadgeType: String, CaseIterable {
case generic
case messages
}

enum BadgeOperation {
case increment
case decrement
}

let INCREMENTED_FOR_KEY = "incremented-for-convos"

// This extension allows us to do some processing of the received notification
// data before displaying the notification to the user. In our use case, there
// are a few particular things that we want to do:
Expand All @@ -30,27 +54,32 @@ class NotificationService: UNNotificationServiceExtension {
private var bestAttempt: UNMutableNotificationContent?

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler

guard let bestAttempt = NSEUtil.createCopy(request.content),
let reason = request.content.userInfo["reason"] as? String
let reasonString = request.content.userInfo["reason"] as? String,
let reason = NotificationType(rawValue: reasonString)
else {
contentHandler(request.content)
return
}


self.contentHandler = contentHandler
self.bestAttempt = bestAttempt
if reason == "chat-message" {
mutateWithChatMessage(bestAttempt)
} else {
mutateWithBadge(bestAttempt)

NSEUtil.shared.prefsQueue.sync {
switch reason {
case .like, .repost, .follow, .reply, .quote, .starterPackJoined:
NSEUtil.mutateWithBadge(bestAttempt, badgeType: .generic, operation: .increment)
case .chatMessage:
NSEUtil.mutateWithChatMessage(bestAttempt)
NSEUtil.mutateWithBadge(bestAttempt, badgeType: .messages, operation: .increment)
case .markReadGeneric:
NSEUtil.mutateWithBadge(bestAttempt, badgeType: .generic, operation: .decrement)
case .markReadMessages:
NSEUtil.mutateWithBadge(bestAttempt, badgeType: .messages, operation: .decrement)
}
contentHandler(bestAttempt)
}

// Any image downloading (or other network tasks) should be handled at the end
// of this block. Otherwise, if there is a timeout and serviceExtensionTimeWillExpire
// gets called, we might not have all the needed mutations completed in time.

contentHandler(bestAttempt)
}

override func serviceExtensionTimeWillExpire() {
Expand All @@ -60,46 +89,106 @@ class NotificationService: UNNotificationServiceExtension {
}
contentHandler(bestAttempt)
}

// MARK: Mutations

func mutateWithBadge(_ content: UNMutableNotificationContent) {
NSEUtil.shared.prefsQueue.sync {
var count = NSEUtil.shared.prefs?.integer(forKey: "badgeCount") ?? 0
count += 1

// Set the new badge number for the notification, then store that value for using later
content.badge = NSNumber(value: count)
NSEUtil.shared.prefs?.setValue(count, forKey: "badgeCount")
}
}

func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
if NSEUtil.shared.prefs?.bool(forKey: "playSoundChat") == true {
mutateWithDmSound(content)
}
}

func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound.default
}

func mutateWithDmSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
}
}

// NSEUtil's purpose is to create a shared instance of `UserDefaults` across
// `NotificationService` instances. It also includes a queue so that we can process
// updates to `UserDefaults` in parallel.
//
// Any time that you increment or decrement counts for notifications, you should use
// the prefsQueue so that things remain in sync.

private class NSEUtil {
static let shared = NSEUtil()

var prefs = UserDefaults(suiteName: APP_GROUP)
var prefsQueue = DispatchQueue(label: "NSEPrefsQueue")

// MARK: - Utils

static func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
return content.mutableCopy() as? UNMutableNotificationContent
}

static func getDecrementedBadgeCount(current: Int, decrementBy by: Int) -> Int {
let new = current - by
if new < 0 {
return 0
}
return new
}

// MARK: - Mutations

static func mutateWithBadge(_ content: UNMutableNotificationContent,
badgeType type: BadgeType,
operation: BadgeOperation) {
var genericCount = Self.shared.prefs?.integer(forKey: BadgeType.generic.rawValue) ?? 0
var messagesCount = Self.shared.prefs?.integer(forKey: BadgeType.messages.rawValue) ?? 0

if type == .generic {
if operation == .decrement {
genericCount = 0
} else {
genericCount += 1
}
Self.shared.prefs?.setValue(genericCount, forKey: BadgeType.generic.rawValue)
// TEMPORARY - since we have not implemented message count clearing on the server, we'll clear
// those here as well.
Self.shared.prefs?.setValue(messagesCount, forKey: BadgeType.messages.rawValue)
} else if type == .messages {
// Not yet implemented, but here's the logic
if operation == .decrement,
Self.shouldDecrementForConvo(content) {
messagesCount = Self.getDecrementedBadgeCount(current: messagesCount, decrementBy: 1)
} else if operation == .increment,
shouldIncrementForConvo(content) {
messagesCount += 1
}
}
}

static func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
if Self.shared.prefs?.bool(forKey: "playSoundChat") == true {
Self.mutateWithDmSound(content)
}
}

static func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound.default
}

static func mutateWithDmSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
}

static func shouldIncrementForConvo(_ content: UNMutableNotificationContent) -> Bool {
guard let convoId = content.userInfo["convoId"] as? String,
var dict = Self.shared.prefs?.dictionary(forKey: INCREMENTED_FOR_KEY) as? [String: Bool] else {
return false
}

if dict["convoId"] == true {
return false
}

dict[convoId] = true
Self.shared.prefs?.set(dict, forKey: INCREMENTED_FOR_KEY)
return true
}

static func shouldDecrementForConvo(_ content: UNMutableNotificationContent) -> Bool {
guard let convoId = content.userInfo["convoId"] as? String,
var dict = Self.shared.prefs?.dictionary(forKey: INCREMENTED_FOR_KEY) as? [String: Bool] else {
return false
}

if dict["convoId"] != true {
return false
}

dict.removeValue(forKey: convoId)
Self.shared.prefs?.set(dict, forKey: INCREMENTED_FOR_KEY)
return true
}
}
123 changes: 62 additions & 61 deletions modules/expo-background-notification-handler/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,88 +6,89 @@ group = 'expo.modules.backgroundnotificationhandler'
version = '0.5.0'

buildscript {
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
if (expoModulesCorePlugin.exists()) {
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
}
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
if (expoModulesCorePlugin.exists()) {
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
}

// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

// Ensures backward compatibility
ext.getKotlinVersion = {
if (ext.has("kotlinVersion")) {
ext.kotlinVersion()
} else {
ext.safeExtGet("kotlinVersion", "1.8.10")
// Ensures backward compatibility
ext.getKotlinVersion = {
if (ext.has("kotlinVersion")) {
ext.kotlinVersion()
} else {
ext.safeExtGet("kotlinVersion", "1.8.10")
}
}
}

repositories {
mavenCentral()
}
repositories {
mavenCentral()
}

dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
}
}

afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
}

android {
compileSdkVersion safeExtGet("compileSdkVersion", 33)
compileSdkVersion safeExtGet("compileSdkVersion", 33)

def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
}
}
}

namespace "expo.modules.backgroundnotificationhandler"
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1
versionName "0.5.0"
}
lintOptions {
abortOnError false
}
publishing {
singleVariant("release") {
withSourcesJar()
namespace "expo.modules.backgroundnotificationhandler"
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1
versionName "0.5.0"
}
lintOptions {
abortOnError false
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation 'com.google.firebase:firebase-messaging-ktx:24.0.0'
implementation project(':expo-bluesky-swiss-army')
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation 'com.google.firebase:firebase-messaging-ktx:24.0.0'
}
Loading
Loading