feat: c2dm prototype via fcm
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
cbd182df34
commit
ff1fc39475
|
|
@ -1,2 +1,3 @@
|
||||||
/build
|
/build
|
||||||
/release
|
/release
|
||||||
|
google-services.json
|
||||||
|
|
@ -11,6 +11,7 @@ plugins {
|
||||||
|
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
|
id 'com.google.gms.google-services'
|
||||||
}
|
}
|
||||||
|
|
||||||
def property(String fileName, String propertyName, String fallbackEnv = null) {
|
def property(String fileName, String propertyName, String fallbackEnv = null) {
|
||||||
|
|
@ -273,6 +274,10 @@ dependencies {
|
||||||
// Livekit
|
// Livekit
|
||||||
// FIXME temporarily not included, re-add when realtime media is to be implemented
|
// FIXME temporarily not included, re-add when realtime media is to be implemented
|
||||||
// implementation "io.livekit:livekit-android:$livekit_version"
|
// implementation "io.livekit:livekit-android:$livekit_version"
|
||||||
|
|
||||||
|
// Firebase - Cloud Messaging
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:33.1.1"))
|
||||||
|
implementation("com.google.firebase:firebase-messaging")
|
||||||
}
|
}
|
||||||
|
|
||||||
sqldelight {
|
sqldelight {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<!-- Up to Android 10, we need the following to take photos from the camera. -->
|
<!-- Up to Android 10, we need the following to take photos from the camera. -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
|
|
@ -49,6 +50,21 @@
|
||||||
android:name="io.sentry.auto-init"
|
android:name="io.sentry.auto-init"
|
||||||
android:value="false" />
|
android:value="false" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||||
|
android:resource="@drawable/ic_notification_monochrome" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_color"
|
||||||
|
android:resource="@color/primary" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".c2dm.HandlerService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
||||||
|
|
@ -145,4 +145,29 @@ object ULID {
|
||||||
|
|
||||||
return timestamp
|
return timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Like asTimestamp, but for the entire ULID including the entropy, as an integer.
|
||||||
|
Note that the number you receive will not be reversible to the original ULID, because
|
||||||
|
it will overflow the `Int` type! Used for generating Android notification IDs.
|
||||||
|
*/
|
||||||
|
fun asInteger(ulid: String): Int {
|
||||||
|
if (ulid.length != len) {
|
||||||
|
throw IllegalArgumentException("ULID must be exactly $len characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
var integer = 0
|
||||||
|
|
||||||
|
for (i in 0 until len) {
|
||||||
|
val char = ulid[i]
|
||||||
|
val value = b32chars.indexOf(char)
|
||||||
|
|
||||||
|
if (value == -1) {
|
||||||
|
throw IllegalArgumentException("Invalid character '$char' at position $i")
|
||||||
|
}
|
||||||
|
|
||||||
|
integer = integer or (value shl (len - i - 1) * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
return integer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package chat.revolt.api.realtime
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import chat.revolt.RevoltApplication
|
||||||
import chat.revolt.api.REVOLT_WEBSOCKET
|
import chat.revolt.api.REVOLT_WEBSOCKET
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.RevoltHttp
|
import chat.revolt.api.RevoltHttp
|
||||||
|
|
@ -37,6 +38,7 @@ import chat.revolt.api.schemas.Channel
|
||||||
import chat.revolt.api.schemas.ChannelType
|
import chat.revolt.api.schemas.ChannelType
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
import chat.revolt.api.settings.SyncedSettings
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
|
import chat.revolt.c2dm.ChannelRegistrator
|
||||||
import chat.revolt.persistence.Database
|
import chat.revolt.persistence.Database
|
||||||
import chat.revolt.persistence.SqlStorage
|
import chat.revolt.persistence.SqlStorage
|
||||||
import io.ktor.client.plugins.websocket.ws
|
import io.ktor.client.plugins.websocket.ws
|
||||||
|
|
@ -63,6 +65,8 @@ object RealtimeSocket {
|
||||||
val database = Database(SqlStorage.driver)
|
val database = Database(SqlStorage.driver)
|
||||||
var socket: WebSocketSession? = null
|
var socket: WebSocketSession? = null
|
||||||
|
|
||||||
|
private val channelRegistrator = ChannelRegistrator(RevoltApplication.instance)
|
||||||
|
|
||||||
private var _disconnectionState = mutableStateOf(DisconnectionState.Reconnecting)
|
private var _disconnectionState = mutableStateOf(DisconnectionState.Reconnecting)
|
||||||
val disconnectionState: DisconnectionState
|
val disconnectionState: DisconnectionState
|
||||||
get() = _disconnectionState.value
|
get() = _disconnectionState.value
|
||||||
|
|
@ -201,6 +205,9 @@ object RealtimeSocket {
|
||||||
val emojiMap = readyFrame.emojis.associateBy { it.id!! }
|
val emojiMap = readyFrame.emojis.associateBy { it.id!! }
|
||||||
RevoltAPI.emojiCache.putAll(emojiMap)
|
RevoltAPI.emojiCache.putAll(emojiMap)
|
||||||
|
|
||||||
|
Log.d("RealtimeSocket", "Registering push notification channels.")
|
||||||
|
channelRegistrator.register()
|
||||||
|
|
||||||
RevoltAPI.closeHydration()
|
RevoltAPI.closeHydration()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,16 @@ import android.util.Log
|
||||||
import chat.revolt.api.RevoltError
|
import chat.revolt.api.RevoltError
|
||||||
import chat.revolt.api.RevoltHttp
|
import chat.revolt.api.RevoltHttp
|
||||||
import chat.revolt.api.RevoltJson
|
import chat.revolt.api.RevoltJson
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.http.*
|
import io.ktor.client.statement.HttpResponse
|
||||||
import kotlinx.serialization.*
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoginNegotiation(
|
data class LoginNegotiation(
|
||||||
|
|
@ -132,10 +138,12 @@ suspend fun negotiateAuthentication(email: String, password: String): EmailPassw
|
||||||
"Success" -> EmailPasswordAssessment(
|
"Success" -> EmailPasswordAssessment(
|
||||||
firstUserHints = RevoltJson.decodeFromString(UserHints.serializer(), responseContent)
|
firstUserHints = RevoltJson.decodeFromString(UserHints.serializer(), responseContent)
|
||||||
)
|
)
|
||||||
|
|
||||||
"MFA" -> EmailPasswordAssessment(
|
"MFA" -> EmailPasswordAssessment(
|
||||||
proceedMfa = true,
|
proceedMfa = true,
|
||||||
mfaSpec = RevoltJson.decodeFromString(MfaLoginSpec.serializer(), responseContent)
|
mfaSpec = RevoltJson.decodeFromString(MfaLoginSpec.serializer(), responseContent)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> throw Exception("Unknown result: ${responseJson.result}")
|
else -> throw Exception("Unknown result: ${responseJson.result}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package chat.revolt.api.routes.push
|
||||||
|
|
||||||
|
import chat.revolt.api.RevoltHttp
|
||||||
|
import chat.revolt.api.routes.account.WebPushData
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
|
||||||
|
suspend fun subscribePush(
|
||||||
|
endpoint: String = "fcm",
|
||||||
|
auth: String,
|
||||||
|
p256diffieHellman: String? = null,
|
||||||
|
) {
|
||||||
|
val data = WebPushData(
|
||||||
|
endpoint = endpoint,
|
||||||
|
p256diffieHellman = p256diffieHellman ?: "",
|
||||||
|
auth = auth
|
||||||
|
)
|
||||||
|
|
||||||
|
RevoltHttp.post("/push/subscribe") {
|
||||||
|
setBody(data)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package chat.revolt.c2dm
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationChannelGroup
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
|
import chat.revolt.R
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// * Add the remaining groups.
|
||||||
|
// * Add the remaining channels.
|
||||||
|
// * Find out whether every conversation should have its own channel or if they should be grouped
|
||||||
|
// together as one channel.
|
||||||
|
|
||||||
|
class ChannelRegistrator(val context: Context) {
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL_ID_GROUP_CONVERSATIONS = "chat.revolt.c2dm.conversations"
|
||||||
|
|
||||||
|
const val CHANNEL_ID_GROUP_SOCIAL = "chat.revolt.c2dm.social"
|
||||||
|
const val CHANNEL_ID_GROUP_SOCIAL_FRIENDREQUESTS = "chat.revolt.c2dm.social.friendrequests"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val notificationManager =
|
||||||
|
getSystemService(context, NotificationManager::class.java) as NotificationManager
|
||||||
|
|
||||||
|
private fun registerGroups() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannelGroup(
|
||||||
|
NotificationChannelGroup(
|
||||||
|
CHANNEL_ID_GROUP_CONVERSATIONS,
|
||||||
|
context.getString(R.string.notification_channel_group_conversations)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
notificationManager.createNotificationChannelGroup(
|
||||||
|
NotificationChannelGroup(
|
||||||
|
CHANNEL_ID_GROUP_SOCIAL,
|
||||||
|
context.getString(R.string.notification_channel_group_social)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerChannels() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_ID_GROUP_SOCIAL_FRIENDREQUESTS,
|
||||||
|
context.getString(R.string.notification_channel_friend_requests),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
).apply {
|
||||||
|
group = CHANNEL_ID_GROUP_SOCIAL
|
||||||
|
description =
|
||||||
|
context.getString(R.string.notification_channel_friend_requests_description)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register() {
|
||||||
|
registerGroups()
|
||||||
|
registerChannels()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package chat.revolt.c2dm
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.Person
|
||||||
|
import androidx.core.app.RemoteInput
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.activities.MainActivity
|
||||||
|
import chat.revolt.api.internals.SpecialUsers
|
||||||
|
import chat.revolt.api.internals.ULID
|
||||||
|
import chat.revolt.api.routes.push.subscribePush
|
||||||
|
import chat.revolt.c2dm.ChannelRegistrator.Companion.CHANNEL_ID_GROUP_SOCIAL_FRIENDREQUESTS
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
class HandlerService : FirebaseMessagingService() {
|
||||||
|
override fun onNewToken(token: String) {
|
||||||
|
super.onNewToken(token)
|
||||||
|
runBlocking {
|
||||||
|
subscribePush(auth = token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
|
val integ = ULID.asInteger(ULID.makeNext())
|
||||||
|
val bitmap = Glide.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.load("https://autumn.revolt.chat/avatars/GJXjHUC1X7tGEgHFhYOvtCByuNRd72qFwlztjKZUHP/bfe0842b74ae716139138574e8a1b749751e2968~3.jpg")
|
||||||
|
.circleCrop()
|
||||||
|
.submit()
|
||||||
|
.get()
|
||||||
|
val jen =
|
||||||
|
Person.Builder()
|
||||||
|
.setBot(true)
|
||||||
|
.setKey(SpecialUsers.JENNIFER)
|
||||||
|
.setIcon(IconCompat.createWithBitmap(bitmap))
|
||||||
|
.setName("Jennifer")
|
||||||
|
.build()
|
||||||
|
var remoteInput = RemoteInput.Builder("Reply").run {
|
||||||
|
setLabel("Reply")
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
var action: NotificationCompat.Action =
|
||||||
|
NotificationCompat.Action.Builder(
|
||||||
|
R.drawable.ic_reply_24dp,
|
||||||
|
"Reply", PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
Intent(this, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_MUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addRemoteInput(remoteInput)
|
||||||
|
.build()
|
||||||
|
var builder = NotificationCompat.Builder(this, CHANNEL_ID_GROUP_SOCIAL_FRIENDREQUESTS)
|
||||||
|
.setSmallIcon(R.drawable.ic_message_text_24dp)
|
||||||
|
.setContentTitle("Jennifer")
|
||||||
|
.setContentText("Expand for details")
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
|
.setStyle(
|
||||||
|
NotificationCompat.MessagingStyle(jen)
|
||||||
|
.setConversationTitle("#Genderal")
|
||||||
|
.addMessage("hiii 👋", System.currentTimeMillis(), jen)
|
||||||
|
)
|
||||||
|
.addAction(action)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).apply {
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this@HandlerService,
|
||||||
|
android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notify(integ, builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -99,7 +99,7 @@ fun UserAvatar(
|
||||||
) {
|
) {
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
RemoteImage(
|
RemoteImage(
|
||||||
url = rawUrl ?: "$REVOLT_FILES/avatars/${avatar.id!!}/user.png?max_side=256",
|
url = rawUrl ?: "$REVOLT_FILES/avatars/${avatar.id}/user.png?max_side=256",
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
description = stringResource(id = R.string.avatar_alt, username),
|
description = stringResource(id = R.string.avatar_alt, username),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package chat.revolt.dialogs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.revolt.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationRationaleDialog(
|
||||||
|
onSelected: (Boolean) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {
|
||||||
|
Text(stringResource(id = R.string.spark_notifications_rationale))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ux_notification_rationale),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 200.dp)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.spark_notifications_rationale_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onSelected(false)
|
||||||
|
onDismiss()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(id = R.string.spark_notifications_rationale_dismiss))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = {
|
||||||
|
onSelected(true)
|
||||||
|
onDismiss()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(id = R.string.spark_notifications_rationale_cta))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
package chat.revolt.screens.settings
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
|
@ -8,9 +13,11 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ElevatedButton
|
import androidx.compose.material3.ElevatedButton
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
|
@ -24,10 +31,15 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
|
@ -35,9 +47,17 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.routes.push.subscribePush
|
||||||
|
import chat.revolt.dialogs.NotificationRationaleDialog
|
||||||
import chat.revolt.persistence.Database
|
import chat.revolt.persistence.Database
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import chat.revolt.persistence.SqlStorage
|
import chat.revolt.persistence.SqlStorage
|
||||||
|
import chat.revolt.ui.theme.FragmentMono
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.android.gms.common.ConnectionResult
|
||||||
|
import com.google.android.gms.common.GoogleApiAvailabilityLight
|
||||||
|
import com.google.android.gms.tasks.OnCompleteListener
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -72,8 +92,85 @@ fun DebugSettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: DebugSettingsScreenViewModel = hiltViewModel()
|
viewModel: DebugSettingsScreenViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
||||||
|
var showNotificationsRationaleDialogue by remember { mutableStateOf(false) }
|
||||||
|
val askNotificationsPermission =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
FirebaseMessaging.getInstance().token.addOnCompleteListener(
|
||||||
|
OnCompleteListener { task ->
|
||||||
|
if (!task.isSuccessful) {
|
||||||
|
Log.e(
|
||||||
|
"DebugSettingsScreen",
|
||||||
|
"Fetching FCM registration token failed",
|
||||||
|
task.exception
|
||||||
|
)
|
||||||
|
return@OnCompleteListener
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
subscribePush(auth = task.result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var showC2dmDataDialogue by remember { mutableStateOf(false) }
|
||||||
|
var fcmToken by remember { mutableStateOf("") }
|
||||||
|
var playServicesAvailable by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showNotificationsRationaleDialogue) {
|
||||||
|
NotificationRationaleDialog(
|
||||||
|
onSelected = { accepted ->
|
||||||
|
if (accepted) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
askNotificationsPermission.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Will not ask for permission on pre-Tiramisu devices!",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showNotificationsRationaleDialogue = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showC2dmDataDialogue) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
showC2dmDataDialogue = false
|
||||||
|
},
|
||||||
|
title = { Text("Notification Properties") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text("FCM token", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
SelectionContainer {
|
||||||
|
Text(fcmToken, fontFamily = FragmentMono)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
"Google Play Services available",
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Text(if (playServicesAvailable) "Yes" else "No")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showC2dmDataDialogue = false
|
||||||
|
}) {
|
||||||
|
Text("OK")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -126,6 +223,42 @@ fun DebugSettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Notifications",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier.padding(bottom = 10.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.horizontalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
ElevatedButton(onClick = { showNotificationsRationaleDialogue = true }) {
|
||||||
|
Text("Show notification rationale dialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevatedButton(onClick = {
|
||||||
|
playServicesAvailable = GoogleApiAvailabilityLight.getInstance()
|
||||||
|
.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
|
||||||
|
FirebaseMessaging.getInstance().token.addOnCompleteListener(
|
||||||
|
OnCompleteListener { task ->
|
||||||
|
if (!task.isSuccessful) {
|
||||||
|
Log.e(
|
||||||
|
"DebugSettingsScreen",
|
||||||
|
"Fetching FCM registration token failed",
|
||||||
|
task.exception
|
||||||
|
)
|
||||||
|
fcmToken = "Fetching FCM registration token failed!"
|
||||||
|
showC2dmDataDialogue = true
|
||||||
|
return@OnCompleteListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fcmToken = task.result
|
||||||
|
showC2dmDataDialogue = true
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
Text("Show Notification Properties")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Changelogs",
|
text = "Changelogs",
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
|
@ -154,7 +287,7 @@ fun DebugSettingsScreen(
|
||||||
Text(
|
Text(
|
||||||
text = viewModel.serverQueries[index].toString(),
|
text = viewModel.serverQueries[index].toString(),
|
||||||
style = LocalTextStyle.current.copy(
|
style = LocalTextStyle.current.copy(
|
||||||
fontFamily = FontFamily.Monospace
|
fontFamily = FragmentMono
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
HorizontalDivider(Modifier.padding(vertical = 10.dp))
|
HorizontalDivider(Modifier.padding(vertical = 10.dp))
|
||||||
|
|
@ -169,7 +302,7 @@ fun DebugSettingsScreen(
|
||||||
Text(
|
Text(
|
||||||
text = viewModel.channelQueries[index].toString(),
|
text = viewModel.channelQueries[index].toString(),
|
||||||
style = LocalTextStyle.current.copy(
|
style = LocalTextStyle.current.copy(
|
||||||
fontFamily = FontFamily.Monospace
|
fontFamily = FragmentMono
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
HorizontalDivider(Modifier.padding(vertical = 10.dp))
|
HorizontalDivider(Modifier.padding(vertical = 10.dp))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="200dp"
|
||||||
|
android:height="200dp"
|
||||||
|
android:viewportWidth="500"
|
||||||
|
android:viewportHeight="500">
|
||||||
|
<path
|
||||||
|
android:pathData="M122.59,447C122.59,326.35 122.59,247.26 122.59,144.56L46,53H293.71C323.14,53 348.89,58.32 370.95,68.97C393.02,79.61 410.19,94.94 422.45,114.95C434.71,134.96 440.84,158.94 440.84,186.9C440.84,215.12 434.51,238.91 421.87,258.27C409.35,277.64 391.73,292.26 369.02,302.14C346.43,312.01 320.04,316.95 289.84,316.95H187.63V233.84H268.16C280.81,233.84 291.59,232.3 300.49,229.22C309.52,226.02 316.43,220.95 321.2,214.02C326.11,207.1 328.56,198.06 328.56,186.9C328.56,175.61 326.11,166.44 321.2,159.39C316.43,152.21 309.52,146.95 300.49,143.61C291.59,140.15 280.81,138.42 268.16,138.42H230.22V447H122.59ZM354.89,266.16L454,447H337.08L240.29,266.16H354.89Z"
|
||||||
|
android:fillColor="@color/primary"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="512dp" android:viewportHeight="132.29" android:viewportWidth="132.29" android:width="512dp">
|
||||||
|
|
||||||
|
<path android:fillColor="?attr/colorSecondary" android:pathData="m103.09,21.16 l2.77,11.75c0.34,1.44 -0.56,2.89 -2,3.23l-18.27,4.31 -3.99,6.45 -5.54,-23.5c-0.34,-1.44 0.56,-2.89 2,-3.23l13.18,-3.11c-0.04,0.42 0.08,0.94 0.18,1.34 0.86,3.65 4.41,5.85 8.07,4.99 1.44,-0.34 2.75,-1.2 3.61,-2.23m-9.06,-3.37c0.52,2.22 2.62,3.52 4.84,2.99 2.22,-0.52 3.52,-2.62 2.99,-4.84 -0.52,-2.22 -2.62,-3.52 -4.84,-2.99 -2.22,0.52 -3.52,2.62 -2.99,4.84z" android:strokeWidth="1.34108"/>
|
||||||
|
|
||||||
|
<path android:fillColor="?attr/colorPrimary" android:pathData="m56.15,88.62v2.46H11.96v-2.46l4.91,-4.91V68.98c0,-7.61 4.98,-14.31 12.28,-16.47 0,-0.25 0,-0.47 0,-0.71a4.91,4.91 0,0 1,4.91 -4.91,4.91 4.91,0 0,1 4.91,4.91c0,0.25 0,0.47 0,0.71 7.29,2.16 12.28,8.86 12.28,16.47V83.71l4.91,4.91m-17.19,4.91a4.91,4.91 0,0 1,-4.91 4.91,4.91 4.91,0 0,1 -4.91,-4.91m23.94,-43.72 l-3.49,3.49c4.2,4.15 6.55,9.8 6.55,15.69h4.91c0,-7.19 -2.85,-14.12 -7.98,-19.17M7.05,68.98h4.91c0,-5.89 2.36,-11.54 6.55,-15.69L15.02,49.81C9.89,54.87 7.05,61.79 7.05,68.98Z" android:strokeColor="#00000000" android:strokeWidth="2.45499"/>
|
||||||
|
|
||||||
|
<path android:fillColor="?attr/colorTertiary" android:pathData="m117.49,66.53c-1.27,-0.59 -2.83,-0.03 -3.42,1.24 -0.59,1.27 -0.03,2.83 1.24,3.42 1.27,0.59 2.83,0.03 3.42,-1.24 0.59,-1.27 0.03,-2.83 -1.24,-3.42m-5.09,1.68 l-6.13,-2.88c-0.73,-0.34 -1.05,-1.22 -0.71,-1.96l4.38,-9.32c0.35,-0.74 1.22,-1.05 1.96,-0.71l0.67,0.31 0.63,-1.33 1.33,0.63 -0.63,1.33 5.33,2.5 0.63,-1.33 1.33,0.63 -0.63,1.33 0.67,0.31c0.73,0.34 1.05,1.22 0.71,1.96l-2.88,6.13c-0.29,-0.24 -0.6,-0.45 -0.93,-0.61 -0.11,-0.05 -0.22,-0.1 -0.34,-0.14l1.88,-4.02 -9.32,-4.38 -3.44,7.32 6.01,2.82c-0.07,0.1 -0.12,0.21 -0.18,0.32 -0.16,0.33 -0.26,0.69 -0.33,1.06z" android:strokeWidth="0.735562"/>
|
||||||
|
|
||||||
|
<path android:fillColor="?attr/colorError" android:pathData="m112.36,116.79 l-0.45,3.03 -18.17,-2.73 0.45,-3.03c0,0 0.91,-6.06 9.99,-4.69 9.08,1.36 8.17,7.42 8.17,7.42m-2.5,-14.31a4.59,4.59 8.54,1 0,-5.22 3.86,4.59 4.59,8.54 0,0 5.22,-3.86m3.69,8.39a8.57,8.57 0,0 1,1.83 6.37l-0.45,3.03 4.54,0.68 0.45,-3.03c0,0 0.78,-5.22 -6.37,-7.06m0.01,-12.47a4.45,4.45 0,0 0,-1.38 0.01,7.65 7.65,0 0,1 -1.3,8.66 4.45,4.45 0,0 0,1.32 0.41,4.59 4.59,8.54 0,0 1.36,-9.08m-16.27,5.3 l-4.54,-0.68 0.68,-4.54 -3.03,-0.45 -0.68,4.54 -4.54,-0.68 -0.45,3.03 4.54,0.68 -0.68,4.54 3.03,0.45 0.68,-4.54 4.54,0.68z" android:strokeWidth="1.53082"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="primary">#FFDA4E5B</color>
|
<color name="primary">#FFFE4654</color>
|
||||||
<color name="background">#FF131722</color>
|
<color name="background">#FF131722</color>
|
||||||
<color name="splashscreen_background">#FFFFFFFF</color>
|
<color name="splashscreen_background">#FFFFFFFF</color>
|
||||||
<color name="foreground">#FF000000</color>
|
<color name="foreground">#FF000000</color>
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,11 @@
|
||||||
<string name="spark_sidebar_settings_tutorial_description_2">Then long tap your profile picture to open the settings.</string>
|
<string name="spark_sidebar_settings_tutorial_description_2">Then long tap your profile picture to open the settings.</string>
|
||||||
<string name="spark_sidebar_settings_tutorial_acknowledge">Got it</string>
|
<string name="spark_sidebar_settings_tutorial_acknowledge">Got it</string>
|
||||||
|
|
||||||
|
<string name="spark_notifications_rationale">Stay in the loop</string>
|
||||||
|
<string name="spark_notifications_rationale_description">Enable notifications to be kept up to date with messages and mentions.</string>
|
||||||
|
<string name="spark_notifications_rationale_cta">Enable notifications</string>
|
||||||
|
<string name="spark_notifications_rationale_dismiss">Not now</string>
|
||||||
|
|
||||||
<string name="notice_platform_mod_dm_title">Important notice regarding your account</string>
|
<string name="notice_platform_mod_dm_title">Important notice regarding your account</string>
|
||||||
<string name="notice_platform_mod_dm_description">You have received an important notice regarding your account from our moderation team. Please read it carefully.</string>
|
<string name="notice_platform_mod_dm_description">You have received an important notice regarding your account from our moderation team. Please read it carefully.</string>
|
||||||
<string name="notice_platform_mod_dm_acknowledge">View</string>
|
<string name="notice_platform_mod_dm_acknowledge">View</string>
|
||||||
|
|
@ -611,4 +616,9 @@
|
||||||
<string name="share_target_attachment_too_large">This attachment is too large for Revolt (max. $1$s).</string>
|
<string name="share_target_attachment_too_large">This attachment is too large for Revolt (max. $1$s).</string>
|
||||||
<string name="share_target_search_channels">Search channels</string>
|
<string name="share_target_search_channels">Search channels</string>
|
||||||
<string name="share_target_select_channel">m select a channel to share to.</string>
|
<string name="share_target_select_channel">m select a channel to share to.</string>
|
||||||
|
|
||||||
|
<string name="notification_channel_group_conversations">Conversations</string>
|
||||||
|
<string name="notification_channel_group_social">Friends and Social</string>
|
||||||
|
<string name="notification_channel_friend_requests">Friend Requests</string>
|
||||||
|
<string name="notification_channel_friend_requests_description">Incoming friend requests, as well as accepted requests you\'ve sent.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ plugins {
|
||||||
id 'com.mikepenz.aboutlibraries.plugin' version "10.9.1" apply false
|
id 'com.mikepenz.aboutlibraries.plugin' version "10.9.1" apply false
|
||||||
id 'com.google.devtools.ksp' version '1.9.22-1.0.17' apply false
|
id 'com.google.devtools.ksp' version '1.9.22-1.0.17' apply false
|
||||||
id 'org.jmailen.kotlinter' version "4.0.0" apply false
|
id 'org.jmailen.kotlinter' version "4.0.0" apply false
|
||||||
|
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('clean', Delete) {
|
tasks.register('clean', Delete) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue