Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
bca2e1e
add MutedWord entity
joelmuraguri Dec 17, 2025
5cd2cc4
add PreferenceDao with muted words operations
joelmuraguri Dec 17, 2025
017e6a4
handle db migrations
joelmuraguri Dec 17, 2025
af68f9a
cache muted word
joelmuraguri Dec 17, 2025
dc21525
add OfMutedWord operations and implementation in PrefUpdater
joelmuraguri Dec 17, 2025
451c20f
implement MutedWord logic in repsoitory
joelmuraguri Dec 17, 2025
bece30c
code clean up
joelmuraguri Dec 18, 2025
2ab6e1a
update to use datastore
joelmuraguri Dec 20, 2025
e0c6f0f
define MutedWordsStateHolder with repository logic
joelmuraguri Dec 20, 2025
6a77fd9
define MutedWordStateHolder and sheet UI
joelmuraguri Dec 20, 2025
abe919a
add strings
joelmuraguri Dec 20, 2025
156fcdb
add a unified moderation state for sheets
joelmuraguri Dec 20, 2025
83e8639
clean up
joelmuraguri Dec 20, 2025
524955d
manual DI of moderationState sheets in UI
joelmuraguri Dec 20, 2025
7a6c874
add moderation menu in PostOptions sheet
joelmuraguri Dec 20, 2025
0627f7f
clean up code fix
joelmuraguri Dec 23, 2025
7ae1bb4
use string resource
joelmuraguri Dec 23, 2025
285873a
update to Bulk update
joelmuraguri Dec 23, 2025
40643b8
update to Bulk update
joelmuraguri Dec 23, 2025
7de2e86
use SheetState as a mutator
joelmuraguri Dec 23, 2025
62c6447
code clean up
joelmuraguri Dec 23, 2025
5e730bf
Add dependencies and initial integration for push notifications on An…
tunjid Dec 24, 2025
34acd39
PR feedback
tunjid Dec 24, 2025
435b2d8
Make debug firebase key more obvious
tunjid Dec 24, 2025
e4be777
clean ups
joelmuraguri Dec 24, 2025
62e1caa
update to pass state directly; remove mutator
joelmuraguri Dec 24, 2025
c35bc52
Decode googgle services json
tunjid Dec 24, 2025
72535d7
Add notification permissions
tunjid Dec 25, 2025
5e0042e
Update build action
tunjid Dec 25, 2025
427c6cd
Update build script
tunjid Dec 25, 2025
ef8673a
Update build script
tunjid Dec 25, 2025
c72336d
Update build script
tunjid Dec 25, 2025
fb524f9
Merge pull request #746 from tunjid/tj/notifications-1
tunjid Dec 25, 2025
f6d1e30
Make moderation option part of post options
tunjid Dec 25, 2025
50f0ebc
Fix compilation
tunjid Dec 25, 2025
57b6b5e
Fix invocation of onShown
tunjid Dec 25, 2025
fcdf729
Remove unused imports
tunjid Dec 25, 2025
4842d4f
Merge pull request #738 from tunjid/joel/moderation-muted-words-impl
tunjid Dec 25, 2025
b87e599
Simplify signature of NetworkMonitor.runCatchingWithNetworkRetry
tunjid Dec 26, 2025
1b3b2e6
Merge pull request #747 from tunjid/tj/network-monitor-simplification
tunjid Dec 26, 2025
08ff75d
Wiring of push notifications delivery
tunjid Dec 26, 2025
d7f69c4
PR feedback
tunjid Dec 26, 2025
4abc566
PR feedback
tunjid Dec 26, 2025
cde0c45
PR feedback
tunjid Dec 26, 2025
cd6d3f3
lint
tunjid Dec 26, 2025
6d55c6d
Merge pull request #748 from tunjid/tj/push-notifications-2
tunjid Dec 27, 2025
9a8a790
Move push notification strings to ui:core for reuse
tunjid Dec 27, 2025
71244b4
PR feedback
tunjid Dec 27, 2025
09c1c56
Merge pull request #749 from tunjid/tj/push-notifications-3
tunjid Dec 27, 2025
2eb1305
Add Notifier and uutilities for creating Android push ntifications
tunjid Dec 27, 2025
c5c881a
Update notification icon
tunjid Dec 27, 2025
3e4c4cb
PR feedback
tunjid Dec 27, 2025
16edbf5
Notification uri as id
tunjid Dec 27, 2025
ca8c55e
Merge pull request #750 from tunjid/tj/push-notifications-4
tunjid Dec 27, 2025
937081b
Improve perf of parsing record uris
tunjid Dec 29, 2025
4f9a55e
PR feedback
tunjid Dec 29, 2025
c946797
Merge pull request #752 from tunjid/tj/record-uri-perf
tunjid Dec 29, 2025
386de0d
Notification processing WIP
tunjid Dec 27, 2025
ff966ae
Fix Notification.deepLinkPath()
tunjid Dec 27, 2025
a44e50b
TID timestamp extraction fix
tunjid Dec 27, 2025
909787f
Search for notifications after push payload
tunjid Dec 28, 2025
b3772f7
lint
tunjid Dec 28, 2025
f9829f2
PR feedback
tunjid Dec 28, 2025
9a0baef
PR feedback
tunjid Dec 28, 2025
5691654
Rebase on main
tunjid Dec 29, 2025
dc078f1
Fix pending intent bugs
tunjid Dec 29, 2025
04aedc4
Temporary RecordUri overlap
tunjid Dec 29, 2025
67ef91b
Merge pull request #751 from tunjid/tj/push-notifications-5
tunjid Dec 29, 2025
9c7cbbc
Disambiguate between RecordUri and EmbeddableRecordUri
tunjid Dec 29, 2025
d822fee
PR feedback
tunjid Dec 29, 2025
ec094a2
Merge pull request #753 from tunjid/tj/embeddable-records
tunjid Dec 29, 2025
94d4dfe
Replace GenericUri for some uris with concrete types
tunjid Dec 29, 2025
08a0d02
Merge pull request #754 from tunjid/tj/uri-cleanup
tunjid Dec 29, 2025
4f9ee96
Add AppBarButton
tunjid Dec 29, 2025
1a13384
ktlint
tunjid Dec 29, 2025
7ee1315
PR feedback
tunjid Dec 29, 2025
a613edd
Merge pull request #755 from tunjid/tj/app-bar-button
tunjid Dec 29, 2025
32d74c7
Implement push notifications request UI
tunjid Dec 29, 2025
9eca5fc
permission rationale fixes
tunjid Dec 29, 2025
fc06d3e
PR feedback
tunjid Dec 29, 2025
b6c5845
Run ktlint
tunjid Dec 29, 2025
6411fc0
Code clean up
tunjid Dec 29, 2025
9c38a0a
Merge pull request #756 from tunjid/tj/push-notifications-6
tunjid Dec 29, 2025
a19c99f
Stronger typing for embeddable records
tunjid Dec 30, 2025
cff0f8b
Merge pull request #758 from tunjid/tj/embeddable-record-typing
tunjid Dec 30, 2025
c0dde45
Improve push notification resolution
tunjid Dec 30, 2025
95ce292
Significantly clean up processing logic
tunjid Dec 30, 2025
b773933
PR feedback
tunjid Dec 30, 2025
85d7b49
Merge pull request #759 from tunjid/tj/push-notifications-7
tunjid Dec 30, 2025
0e6eac6
Update push notifications UI logic
tunjid Dec 30, 2025
9fbe930
Fix typo
tunjid Dec 30, 2025
cf6d0ac
Merge pull request #760 from tunjid/tj/push-notifications-rationale
tunjid Dec 30, 2025
9b4ea53
Add platform specific network connection error checking
tunjid Dec 31, 2025
1a6f5b6
PR feedback
tunjid Dec 31, 2025
92c4b43
Merge pull request #761 from tunjid/tj/platform-network-connection-ex…
tunjid Dec 31, 2025
e1708d7
Stop checking for notifcation unread counts in the background
tunjid Dec 31, 2025
a5e819a
Fix ToggleUnreadNotificationsMonitor key
tunjid Dec 31, 2025
8defe5e
PR feedback
tunjid Dec 31, 2025
f8e0a3d
Merge pull request #762 from tunjid/tj/lifecycle-notification-observa…
tunjid Dec 31, 2025
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
27 changes: 23 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,29 @@ jobs:
build-android-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
- name: Checkout repo
uses: actions/checkout@v5

- name: Setup java
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew assembleRelease

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Create Google services file
shell: bash
env:
GOOGLE_SERVICES_ENCODED: ${{ secrets.GOOGLE_SERVICES_BASE_64 }}
run: |
# Create the directory structure
mkdir -p "composeApp/src/release/"

# Decode the env variable into the file
echo "$GOOGLE_SERVICES_ENCODED" | openssl base64 -d -A \
-out "composeApp/src/release/google-services.json"

- name: Build App
run: ./gradlew assembleRelease
16 changes: 16 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,28 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Create Google services file
shell: bash
env:
# Map the secret to a shell variable here
GOOGLE_SERVICES_ENCODED: ${{ secrets.GOOGLE_SERVICES_BASE_64 }}
run: |
# Create the directory structure
mkdir -p "composeApp/src/release/"

# Decode the env variable into the file
echo "$GOOGLE_SERVICES_ENCODED" | openssl base64 -d -A \
-out "composeApp/src/release/google-services.json"

- name: Build Unsigned AAB
env:
HERON_ENDPOINT: ${{ secrets.HERON_ENDPOINT }}
run: |
./gradlew spotlessCheck \
bundleRelease \
-Pheron.versionCode=${{ github.run_number }} \
-Pheron.isPlayStore=true
-Pheron.endpoint="$HERON_ENDPOINT"

- name: Sign AAB
uses: r0adkll/sign-android-release@v1
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,31 @@ jobs:
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
id: extract_branch

- name: Checkout repo
uses: actions/checkout@v5

- name: Setup java
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21

- name: Setup gradle
uses: gradle/actions/setup-gradle@v4

- name: Create Google services file
shell: bash
env:
GOOGLE_SERVICES_ENCODED: ${{ secrets.GOOGLE_SERVICES_BASE_64 }}
run: |
# Create the directory structure
mkdir -p "composeApp/src/release/"

# Decode the env variable into the file
echo "$GOOGLE_SERVICES_ENCODED" | openssl base64 -d -A \
-out "composeApp/src/release/google-services.json"

- name: Tag release
run: |
./gradlew release \
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ captures
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
/composeApp/release/composeApp-release.aab
/composeApp/src/release/google-services.json
/composeApp/src/staging/google-services.json
Comment on lines +20 to +21

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While it's great that you've ignored the release and staging google-services.json files, the debug version is being committed in this pull request. It's a security best practice to avoid committing any sensitive configuration files, even for debug builds with mock data. This prevents accidental exposure of real keys and establishes a good pattern for handling secrets. I recommend ignoring all google-services.json files and providing a template file instead for developers to use.

/composeApp/src/release/google-services.json
/composeApp/src/debug/google-services.json
/composeApp/src/staging/google-services.json

/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
/dependencies.txt
/dependency-tree-diff.jar
Expand Down
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ plugins {
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.axionRelease) apply false
alias(libs.plugins.buildConfig) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.googleServices) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.androidxRoom) apply false
Expand All @@ -33,7 +36,6 @@ plugins {
alias(libs.plugins.spotless) apply false
alias(libs.plugins.aboutLibraries) apply false
alias(libs.plugins.burst) apply false
alias(libs.plugins.axionRelease) apply false
}

allprojects {
Expand Down
4 changes: 4 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ plugins {
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.axionRelease)
alias(libs.plugins.googleServices)
id("ksp-convention")
id("kotlin-jvm-convention")
}
Expand Down Expand Up @@ -72,6 +73,9 @@ kotlin {
implementation(libs.connectivity.device)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.splashscreen)

implementation(project.dependencies.platform(libs.firebase.bom))
implementation(libs.firebase.messaging)
}
commonMain.dependencies {
implementation(project(":data:models"))
Expand Down
13 changes: 13 additions & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<application
android:name=".HeronApplication"
Expand All @@ -25,6 +26,18 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Heron">
<service
android:name=".NotificationsService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver android:name=".NotificationDismissReceiver" android:exported="false">
<intent-filter>
<action android:name="com.tunjid.heron.ACTION_NOTIFICATION_DISMISS" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:configChanges="uiMode"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@
package com.tunjid.heron

import android.app.Application
import com.tunjid.heron.scaffold.scaffold.AppState

class HeronApplication : Application() {

val appState by lazy {
createAppState(this)
// This needs to be lateinit instead of lazy to ensure it is
// instantiated on the main thread
lateinit var appState: AppState

override fun onCreate() {
super.onCreate()
appState = createAppState(this)
}
}
28 changes: 28 additions & 0 deletions composeApp/src/androidMain/kotlin/com/tunjid/heron/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.app.NotificationManagerCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.splashscreen.SplashScreenViewProvider
import androidx.core.view.WindowCompat
import com.google.firebase.messaging.FirebaseMessaging
import com.tunjid.heron.data.core.types.GenericUri
import com.tunjid.heron.scaffold.notifications.NotificationAction
import com.tunjid.heron.scaffold.scaffold.App
import com.tunjid.heron.scaffold.scaffold.isShowingSplashScreen
import kotlinx.coroutines.delay
Expand Down Expand Up @@ -75,9 +78,34 @@ class MainActivity : ComponentActivity() {
}
}

updateNotificationPermissions()
handleDeepLink(intent)
}

override fun onResume() {
super.onResume()
updateNotificationPermissions()

FirebaseMessaging.getInstance()
.getToken()
.addOnSuccessListener { token ->
// appState will check if notification permissions are available
appState.onNotificationAction(NotificationAction.RegisterToken(token))
}
}

private fun updateNotificationPermissions() {
NotificationManagerCompat.from(this)
.areNotificationsEnabled()
.let {
appState.onNotificationAction(
NotificationAction.UpdatePermissions(
hasNotificationPermissions = it,
),
)
}
}

private fun handleDeepLink(intent: Intent) {
intent.data
?.let { uri ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2024 Adetunji Dahunsi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.tunjid.heron

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tunjid.heron.scaffold.notifications.AndroidNotifier.Companion.DISMISSAL_ACTION
import com.tunjid.heron.scaffold.notifications.AndroidNotifier.Companion.DISMISSAL_INSTANT_EXTRA
import com.tunjid.heron.scaffold.notifications.NotificationAction
import com.tunjid.heron.scaffold.scaffold.AppState
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout

class NotificationsService : FirebaseMessagingService() {

override fun onNewToken(token: String) =
appState.onNotificationAction(NotificationAction.RegisterToken(token = token))

override fun onMessageReceived(message: RemoteMessage) {
val action = NotificationAction.HandleNotification(payload = message.data)
action.senderDid ?: return
val recordUri = action.recordUri ?: return

appState.onNotificationAction(action)

// await processing completion or timeout to prevent the app from being
// killed due to background execution limits.
try {
runBlocking {
withTimeout(AppState.NOTIFICATION_PROCESSING_TIMEOUT_SECONDS) {
appState.awaitNotificationProcessing(recordUri)
}
}
} catch (e: Exception) {
// No logging utilities in the app at the moment due to its open source nature.
e.printStackTrace()
} finally {
appState.onNotificationAction(
NotificationAction.NotificationProcessedOrDropped(recordUri),
)
}
}
}

class NotificationDismissReceiver : BroadcastReceiver() {
@OptIn(ExperimentalTime::class)
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != DISMISSAL_ACTION) return

val dismissedAtEpoch = intent.getLongExtra(
/* name = */
DISMISSAL_INSTANT_EXTRA,
/* defaultValue = */
0,
)
if (dismissedAtEpoch > 0) context.appState.onNotificationAction(
NotificationAction.NotificationDismissed(
dismissedAt = Instant.fromEpochMilliseconds(dismissedAtEpoch),
),
)
}
}

private val Context.appState
get() = (applicationContext as HeronApplication).appState
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.tunjid.heron.data.database.getDatabaseBuilder
import com.tunjid.heron.data.di.DataBindingArgs
import com.tunjid.heron.images.imageLoader
import com.tunjid.heron.media.video.ExoplayerController
import com.tunjid.heron.scaffold.notifications.AndroidNotifier
import com.tunjid.heron.scaffold.scaffold.AppState
import dev.jordond.connectivity.Connectivity
import kotlinx.coroutines.Dispatchers
Expand All @@ -40,6 +41,9 @@ fun createAppState(context: Context): AppState =
imageLoader = {
imageLoader(context)
},
notifier = {
AndroidNotifier(context)
},
videoPlayerController = { appScope ->
ExoplayerController(
context = context,
Expand Down
3 changes: 3 additions & 0 deletions composeApp/src/commonMain/kotlin/com/tunjid/heron/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import com.tunjid.heron.profiles.di.ProfilesBindings
import com.tunjid.heron.profiles.di.ProfilesNavigationBindings
import com.tunjid.heron.scaffold.di.ScaffoldBindingArgs
import com.tunjid.heron.scaffold.di.ScaffoldBindings
import com.tunjid.heron.scaffold.notifications.Notifier
import com.tunjid.heron.scaffold.scaffold.AppState
import com.tunjid.heron.search.di.SearchBindings
import com.tunjid.heron.search.di.SearchNavigationBindings
Expand All @@ -77,6 +78,7 @@ expect fun getPlatform(): Platform

fun createAppState(
imageLoader: () -> ImageLoader,
notifier: (appScope: CoroutineScope) -> Notifier,
videoPlayerController: (appScope: CoroutineScope) -> VideoPlayerController,
args: (appScope: CoroutineScope) -> DataBindingArgs,
): AppState {
Expand Down Expand Up @@ -111,6 +113,7 @@ fun createAppState(
val scaffoldBindings = ScaffoldBindings(
args = ScaffoldBindingArgs(
imageLoader = imageLoader(),
notifier = notifier(appScope),
videoPlayerController = videoPlayerController(appScope),
routeMatchers = navigationComponent.allRouteMatchers,
),
Expand Down
Loading