Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ revoltbuild.properties
sentry.properties
/.kotlin/sessions
app/src/main/assets/embedded
/app/src/main/jniLibs/arm64-v8a/
/app/src/main/jniLibs/armeabi-v7a/
/app/src/main/jniLibs/x86/
/app/src/main/jniLibs/x86_64/
# /app/src/main/jniLibs/arm64-v8a/
# /app/src/main/jniLibs/armeabi-v7a/
# /app/src/main/jniLibs/x86/
# /app/src/main/jniLibs/x86_64/
app/release-key.keystore
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Revolt for Android (Forked Version!)

[![Open in DevPod!](https://devpod.sh/assets/open-in-devpod.svg)](https://devpod.sh/open#https://github.com/alexjyong/android)
<div align="center">
<h1>Revolt for Android (Forked Version!)</h1>
<p>Forked Version of the <a href="https://revolt.chat">Revolt</a> Android app.</p>
Expand All @@ -16,18 +16,23 @@
NOTE: This is a forked version of the Android app for the [Revolt](https://revolt.chat) chat platform.
I am not affilated with the Revolt Team, nor is this an official Revolt product.

I made this for some QOL changes that aren't present in the current version at the time of writing.
I made this for some QOL changes that aren't present in the current version at the time of writing such as notification support, jump to reply, voice messages, and more!

**This app also works on de-googled phones as well!**

Feel free to use this for whatever, but note that this is NOT the official Revolt android app. :)
Feel free to use this for whatever, but note that this is NOT the official Revolt Android app. :)

You can download the latest APK [here](https://github.com/alexjyong/android/releases/latest).

For support, discussion, updates and other things, visit our support server on [Revolt](https://rvlt.gg/C7qQMwsZ).

## Features Added
Tap a card to expand.
<details>
<summary><strong>Notification Support!!!🎉🎉</strong></summary>

![Notification support preview](https://github.com/user-attachments/assets/8123962e-e2d3-4690-87e3-44e09724a29c)
</details>

<details>
<summary><strong>Voice messages</strong></summary>
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ android {
minSdk = 24
targetSdk = 36
versionCode = Integer.parseInt("001_003_206".replace("_", ""), 10)
versionName = "1.3.6bg-forked"
versionName = "1.3.6bf-forked"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<!-- Up to Android 10, we need the following to take photos from the camera. -->
<uses-permission
Expand Down Expand Up @@ -65,6 +67,12 @@
</intent-filter>
</service>

<service
android:name=".services.NotificationForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />

<activity
android:name=".activities.MainActivity"
android:exported="true"
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/chat/revolt/RevoltApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ package chat.revolt
import android.app.Application
import android.os.StrictMode
import chat.revolt.internals.EmojiRepository
import chat.revolt.services.NotificationServiceManager
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import javax.inject.Inject

@HiltAndroidApp
class RevoltApplication : Application() {
companion object {
lateinit var instance: RevoltApplication
}

@Inject
lateinit var notificationServiceManager: NotificationServiceManager

private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

override fun onCreate() {
Expand All @@ -39,6 +44,7 @@ class RevoltApplication : Application() {
}

EmojiRepository.initialize(applicationScope)
notificationServiceManager.startServicesIfEnabled(applicationScope)
}

init {
Expand Down
131 changes: 129 additions & 2 deletions app/src/main/java/chat/revolt/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chat.revolt.activities

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Bundle
Expand Down Expand Up @@ -72,6 +73,13 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import chat.revolt.callbacks.Action
import chat.revolt.callbacks.ActionChannel
import chat.revolt.api.internals.CurrentChannelState
import chat.revolt.screens.chat.ChatRouterDestination
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import chat.revolt.BuildConfig
import chat.revolt.R
import chat.revolt.RevoltApplication
Expand Down Expand Up @@ -116,6 +124,7 @@ import chat.revolt.screens.settings.ChatSettingsScreen
import chat.revolt.screens.settings.DebugSettingsScreen
import chat.revolt.screens.settings.ExperimentsSettingsScreen
import chat.revolt.screens.settings.LanguagePickerSettingsScreen
import chat.revolt.screens.settings.NotificationSettingsScreen
import chat.revolt.screens.settings.ProfileSettingsScreen
import chat.revolt.screens.settings.SessionSettingsScreen
import chat.revolt.screens.settings.SettingsScreen
Expand Down Expand Up @@ -144,6 +153,9 @@ class MainActivityViewModel @Inject constructor(
val isReady = MutableStateFlow(false)
val couldNotLogIn = MutableStateFlow(false)

private var pendingChannelId: String? = null
private var pendingMessageId: String? = null

private fun hasInternetConnection(): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
Expand Down Expand Up @@ -285,6 +297,78 @@ class MainActivityViewModel @Inject constructor(
}
}

fun setPendingChannelNavigation(channelId: String, messageId: String? = null) {
Log.d("MainActivity", "Setting pending channel navigation to: $channelId, messageId: $messageId")
pendingChannelId = channelId
pendingMessageId = messageId

updateNextDestination("main/conversation/$channelId")
}

fun processPendingNavigation() {
pendingChannelId?.let { channelId ->
Log.d("MainActivity", "Processing pending navigation to channel: $channelId")

if (!isReady.value) {
viewModelScope.launch {
isReady.first { it }
processPendingNavigationNow()
}
return
}

processPendingNavigationNow()
} ?: run {
System.out.println("REVOLT_DEBUG: processPendingNavigation called but no pending channelId")
}
}

private fun processPendingNavigationNow() {
pendingChannelId?.let { channelId ->
viewModelScope.launch {
try {
// First establish server context for proper back navigation
val channel = RevoltAPI.channelCache[channelId]
val serverId = channel?.server

if (serverId != null) {
Log.d("MainActivity", "Building navigation stack - Server: $serverId, Channel: $channelId")

// First navigate to server context (establishes proper back navigation)
ActionChannel.send(Action.ChatNavigate(ChatRouterDestination.NoCurrentChannel(serverId)))

// Small delay to ensure server context is established
delay(100)

// Then navigate to the specific channel
val messageId = pendingMessageId
if (messageId != null) {
ActionChannel.send(Action.SwitchChannelAndHighlight(channelId, messageId))
Log.d("MainActivity", "Successfully built navigation stack with highlighting: Server($serverId) -> Channel($channelId) -> Message($messageId)")
} else {
ActionChannel.send(Action.SwitchChannel(channelId))
Log.d("MainActivity", "Successfully built navigation stack: Server($serverId) -> Channel($channelId)")
}
} else {
Log.w("MainActivity", "Could not determine server for channel $channelId, falling back to direct navigation")
// Fallback to direct navigation if we can't determine server
val messageId = pendingMessageId
if (messageId != null) {
ActionChannel.send(Action.SwitchChannelAndHighlight(channelId, messageId))
} else {
ActionChannel.send(Action.SwitchChannel(channelId))
}
}

pendingChannelId = null
pendingMessageId = null
} catch (e: Exception) {
Log.e("MainActivity", "Failed to navigate to channel from notification: $channelId", e)
}
}
}
}

val activeAlert = MutableStateFlow<HealthNotice?>(null)
val isAlertActive = MutableStateFlow(false)

Expand Down Expand Up @@ -334,6 +418,15 @@ class MainActivity : AppCompatActivity() {
DynamicColors.applyToActivitiesIfAvailable(RevoltApplication.instance)
@Suppress("DEPRECATION") // We are fixing a bug in the splash screen
window.statusBarColor = Color.Transparent.toArgb()

// Set app as foreground for notification filtering
CurrentChannelState.setAppForegroundState(true)
}

override fun onPause() {
super.onPause()
// Set app as background for notification filtering
CurrentChannelState.setAppForegroundState(false)
}

// Same as above for configuration changes (rotation, dark mode, etc.)
Expand All @@ -345,6 +438,26 @@ class MainActivity : AppCompatActivity() {
window.statusBarColor = Color.Transparent.toArgb()
}

override fun onNewIntent(intent: Intent) {
Log.d("MainActivity", "onNewIntent called")
super.onNewIntent(intent)
processNotificationIntent(intent)
viewModel.processPendingNavigation()
}

private fun processNotificationIntent(intent: Intent) {
val channelId = intent.getStringExtra("channelId")
val messageId = intent.getStringExtra("messageId")
Log.d("MainActivity", "Channel ID from intent: $channelId, Message ID: $messageId")

if (channelId != null) {
Log.d("MainActivity", "Found notification deep link to channel: $channelId, messageId: $messageId")
viewModel.setPendingChannelNavigation(channelId, messageId)
} else {
Log.w("MainActivity", "No channelId found in intent extras")
}
}

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -360,6 +473,8 @@ class MainActivity : AppCompatActivity() {

RevoltAPI.hydrateFromPersistentCache()

processNotificationIntent(intent)

setContent {
val windowSizeClass = calculateWindowSizeClass(this)
AppEntrypoint(
Expand All @@ -373,7 +488,8 @@ class MainActivity : AppCompatActivity() {
viewModel::onDismissHealthAlert,
viewModel::onDismissLoginError,
viewModel::checkLoggedInState,
viewModel::updateNextDestination
viewModel::updateNextDestination,
viewModel::processPendingNavigation
)
}

Expand Down Expand Up @@ -448,7 +564,8 @@ fun AppEntrypoint(
onDismissHealthAlert: () -> Unit = {},
onDismissLoginError: () -> Unit = {},
onRetryConnection: () -> Unit,
onUpdateNextDestination: (String) -> Unit = {}
onUpdateNextDestination: (String) -> Unit = {},
onProcessPendingNavigation: () -> Unit = {}
) {
var showVoiceUI by rememberSaveable { mutableStateOf(false) }
var voiceChannelId by rememberSaveable { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -624,6 +741,11 @@ fun AppEntrypoint(
) + fadeIn(animationSpec = RevoltTweenFloat)
}
) {

LaunchedEffect(Unit) {
onProcessPendingNavigation()
}

ChatRouterScreen(
navController,
windowSizeClass,
Expand Down Expand Up @@ -665,6 +787,10 @@ fun AppEntrypoint(
)
}
) {
LaunchedEffect(Unit) {
onProcessPendingNavigation()
}

MainScreen(navController)
}
composable(
Expand Down Expand Up @@ -712,6 +838,7 @@ fun AppEntrypoint(
composable("settings/sessions") { SessionSettingsScreen(navController) }
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
composable("settings/chat") { ChatSettingsScreen(navController) }
composable("settings/notifications") { NotificationSettingsScreen(navController) }
composable("settings/debug") { DebugSettingsScreen(navController) }
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
Expand Down
39 changes: 39 additions & 0 deletions app/src/main/java/chat/revolt/api/internals/CurrentChannelState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package chat.revolt.api.internals

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/**
* Singleton to track the currently active channel and app state for smart notification filtering
*/
object CurrentChannelState {
private val _currentChannelId = MutableStateFlow<String?>(null)
val currentChannelId: StateFlow<String?> = _currentChannelId.asStateFlow()

private val _isAppInForeground = MutableStateFlow(false)
val isAppInForeground: StateFlow<Boolean> = _isAppInForeground.asStateFlow()

fun setCurrentChannel(channelId: String?) {
_currentChannelId.value = channelId
}

fun getCurrentChannel(): String? = _currentChannelId.value

fun setAppForegroundState(isInForeground: Boolean) {
_isAppInForeground.value = isInForeground
// Clear current channel when app goes to background
if (!isInForeground) {
_currentChannelId.value = null
}
}

fun isAppInForeground(): Boolean = _isAppInForeground.value

/**
* Should filter notifications only if app is in foreground AND user is viewing the specific channel
*/
fun shouldFilterNotification(channelId: String): Boolean {
return isAppInForeground() && getCurrentChannel() == channelId
}
}
Loading