From 3eb8c8f015380da1b9f5319ba869f082f7925970 Mon Sep 17 00:00:00 2001
From: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Thu, 24 Aug 2023 02:15:06 +0200
Subject: [PATCH] feat: streaks reminder - add streak indicator in social - fix
dialogs - fix container global state
---
app/src/main/AndroidManifest.xml | 3 +
.../me/rhunk/snapenhance/RemoteSideContext.kt | 23 ++--
.../rhunk/snapenhance/SharedContextHolder.kt | 1 +
.../rhunk/snapenhance/bridge/BridgeService.kt | 4 +-
.../snapenhance/bridge/ForceStartActivity.kt | 20 ++++
.../snapenhance/messaging/ModDatabase.kt | 6 +-
.../snapenhance/messaging/StreaksReminder.kt | 101 ++++++++++++++++++
.../snapenhance/ui/manager/MainActivity.kt | 8 +-
.../sections/features/FeaturesSection.kt | 25 +++--
.../sections/social/AddFriendDialog.kt | 32 +++---
.../manager/sections/social/ScopeContent.kt | 17 ++-
.../manager/sections/social/SocialSection.kt | 33 ++++--
.../rhunk/snapenhance/ui/util/AlertDialogs.kt | 28 ++---
.../snapenhance/ui/util/ComposeImageHelper.kt | 9 +-
app/src/main/res/drawable/streak_icon.xml | 11 ++
core/src/main/assets/lang/en_US.json | 10 ++
.../rhunk/snapenhance/bridge/BridgeClient.kt | 4 +-
.../snapenhance/bridge/ForceStartActivity.kt | 11 --
.../core/config/ConfigContainer.kt | 3 +-
.../snapenhance/core/config/impl/Global.kt | 4 +-
.../core/config/impl/MessagingTweaks.kt | 4 +-
.../core/config/impl/RootConfig.kt | 1 +
.../snapenhance/core/config/impl/Spoof.kt | 4 +-
.../core/config/impl/StreaksReminderConfig.kt | 7 ++
.../core/messaging/MessagingCoreObjects.kt | 11 +-
.../impl/downloader/MediaDownloader.kt | 6 +-
.../snapenhance/ui/ViewAppearanceHelper.kt | 2 +-
.../ui/menu/impl/FriendFeedInfoMenu.kt | 2 +-
.../snapenhance/util/snap/BitmojiSelfie.kt | 5 +-
29 files changed, 305 insertions(+), 90 deletions(-)
create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt
create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt
create mode 100644 app/src/main/res/drawable/streak_icon.xml
delete mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt
create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/StreaksReminderConfig.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0d436aed..c4cb1785 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,7 @@
+
@@ -61,6 +62,8 @@
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true"
android:exported="true" />
+
+
\ No newline at end of file
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt
index 9e93c132..a523196b 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt
@@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity
import androidx.documentfile.provider.DocumentFile
import coil.ImageLoader
import coil.decode.VideoFrameDecoder
+import coil.disk.DiskCache
import coil.memory.MemoryCache
import kotlinx.coroutines.Dispatchers
import me.rhunk.snapenhance.bridge.BridgeService
@@ -17,6 +18,7 @@ import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper
import me.rhunk.snapenhance.core.config.ModConfig
import me.rhunk.snapenhance.download.DownloadTaskManager
import me.rhunk.snapenhance.messaging.ModDatabase
+import me.rhunk.snapenhance.messaging.StreaksReminder
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
@@ -39,6 +41,7 @@ class RemoteSideContext(
val mappings = MappingsWrapper()
val downloadTaskManager = DownloadTaskManager()
val modDatabase = ModDatabase(this)
+ val streaksReminder = StreaksReminder(this)
//used to load bitmoji selfies and download previews
val imageLoader by lazy {
@@ -48,24 +51,30 @@ class RemoteSideContext(
MemoryCache.Builder(androidContext)
.maxSizePercent(0.25)
.build()
- }.components { add(VideoFrameDecoder.Factory()) }.build()
- }
-
- init {
- reload()
+ }
+ .diskCache {
+ DiskCache.Builder()
+ .directory(androidContext.cacheDir.resolve("coil-disk-cache"))
+ .maxSizeBytes(1024 * 1024 * 100) // 100MB
+ .build()
+ }
+ .components { add(VideoFrameDecoder.Factory()) }.build()
}
fun reload() {
runCatching {
config.loadFromContext(androidContext)
- translation.userLocale = config.locale
- translation.loadFromContext(androidContext)
+ translation.apply {
+ userLocale = config.locale
+ loadFromContext(androidContext)
+ }
mappings.apply {
loadFromContext(androidContext)
init(androidContext)
}
downloadTaskManager.init(androidContext)
modDatabase.init()
+ streaksReminder.init()
}.onFailure {
Logger.error("Failed to load RemoteSideContext", it)
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt
index 31aeb5d8..7da09d39 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt
@@ -9,6 +9,7 @@ object SharedContextHolder {
fun remote(context: Context): RemoteSideContext {
if (!::_remoteSideContext.isInitialized || _remoteSideContext.get() == null) {
_remoteSideContext = WeakReference(RemoteSideContext(context))
+ _remoteSideContext.get()?.reload()
}
return _remoteSideContext.get()!!
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt
index 9d1b35ed..56cec538 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt
@@ -26,7 +26,9 @@ class BridgeService : Service() {
remoteSideContext = SharedContextHolder.remote(this).apply {
if (checkForRequirements()) return null
}
- remoteSideContext.bridgeService = this
+ remoteSideContext.apply {
+ bridgeService = this@BridgeService
+ }
messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() }
return BridgeBinder()
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt
new file mode 100644
index 00000000..ed99d393
--- /dev/null
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt
@@ -0,0 +1,20 @@
+package me.rhunk.snapenhance.bridge
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import me.rhunk.snapenhance.SharedContextHolder
+
+class ForceStartActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (intent.getBooleanExtra("streaks_notification_action", false)) {
+ packageManager.getLaunchIntentForPackage("com.snapchat.android")?.apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(this)
+ }
+ SharedContextHolder.remote(this).streaksReminder.dismissAllNotifications()
+ }
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt
index 4fc385d0..e87c6535 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt
@@ -140,8 +140,11 @@ class ModDatabase(
)
//sync streaks
if (friend.streakLength > 0) {
- database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, length) VALUES (?, ?, ?)", arrayOf(
+ val streaks = getFriendStreaks(friend.userId!!)
+
+ database.execSQL("INSERT OR REPLACE INTO streaks (userId, notify, expirationTimestamp, length) VALUES (?, ?, ?, ?)", arrayOf(
friend.userId,
+ streaks?.notify ?: false,
friend.streakExpirationTimestamp,
friend.streakLength
))
@@ -198,6 +201,7 @@ class ModDatabase(
fun deleteFriend(userId: String) {
executeAsync {
database.execSQL("DELETE FROM friends WHERE userId = ?", arrayOf(userId))
+ database.execSQL("DELETE FROM streaks WHERE userId = ?", arrayOf(userId))
}
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt
new file mode 100644
index 00000000..6a3731da
--- /dev/null
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt
@@ -0,0 +1,101 @@
+package me.rhunk.snapenhance.messaging
+
+import android.app.AlarmManager
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import androidx.core.graphics.drawable.toBitmap
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import me.rhunk.snapenhance.R
+import me.rhunk.snapenhance.RemoteSideContext
+import me.rhunk.snapenhance.SharedContextHolder
+import me.rhunk.snapenhance.bridge.ForceStartActivity
+import me.rhunk.snapenhance.ui.util.ImageRequestHelper
+import me.rhunk.snapenhance.util.snap.BitmojiSelfie
+
+class StreaksReminder(
+ private val remoteSideContext: RemoteSideContext? = null
+): BroadcastReceiver() {
+ companion object {
+ private const val NOTIFICATION_CHANNEL_ID = "streaks"
+ }
+
+ private val coroutineScope = CoroutineScope(Dispatchers.IO)
+
+ private fun getNotificationManager(context: Context) = context.getSystemService(NotificationManager::class.java).apply {
+ createNotificationChannel(
+ NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ "Streaks",
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ )
+ }
+
+ override fun onReceive(ctx: Context, intent: Intent) {
+ val remoteSideContext = this.remoteSideContext ?: SharedContextHolder.remote(ctx)
+ if (remoteSideContext.config.root.streaksReminder.globalState != true) return
+
+ val notifyFriendList = remoteSideContext.modDatabase.getFriends()
+ .associateBy { remoteSideContext.modDatabase.getFriendStreaks(it.userId) }
+ .filter { (streaks, _) -> streaks != null && streaks.notify && streaks.isAboutToExpire() }
+
+ val notificationManager = getNotificationManager(ctx)
+
+ notifyFriendList.forEach { (streaks, friend) ->
+ coroutineScope.launch {
+ val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
+ val bitmojiImage = remoteSideContext.imageLoader.execute(
+ ImageRequestHelper.newBitmojiImageRequest(ctx, bitmojiUrl)
+ )
+
+ val notificationBuilder = NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID)
+ .setContentTitle("Streaks")
+ .setContentText("You will lose streaks with ${friend.displayName} in ${streaks?.hoursLeft() ?: 0} hours")
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentIntent(PendingIntent.getActivity(
+ ctx,
+ 0,
+ Intent(ctx, ForceStartActivity::class.java).apply {
+ putExtra("streaks_notification_action", true)
+ },
+ PendingIntent.FLAG_IMMUTABLE
+ ))
+ .apply {
+ bitmojiImage.drawable?.let {
+ setLargeIcon(it.toBitmap())
+ setSmallIcon(R.drawable.streak_icon)
+ }
+ }
+
+ notificationManager.notify(friend.userId.hashCode(), notificationBuilder.build().apply {
+ flags = NotificationCompat.FLAG_ONLY_ALERT_ONCE
+ })
+ }
+ }
+ }
+
+ //TODO: ask for notifications permission for a13+
+ fun init() {
+ if (remoteSideContext == null) throw IllegalStateException("RemoteSideContext is null")
+ val reminderConfig = remoteSideContext.config.root.streaksReminder.also {
+ if (it.globalState != true) return
+ }
+
+ remoteSideContext.androidContext.getSystemService(AlarmManager::class.java).setRepeating(
+ AlarmManager.RTC_WAKEUP, 5000, reminderConfig.interval.get().toLong() * 60 * 60 * 1000,
+ PendingIntent.getBroadcast(remoteSideContext.androidContext, 0, Intent(remoteSideContext.androidContext, StreaksReminder::class.java),
+ PendingIntent.FLAG_IMMUTABLE)
+ )
+
+ onReceive(remoteSideContext.androidContext, Intent())
+ }
+
+ fun dismissAllNotifications() = getNotificationManager(remoteSideContext!!.androidContext).cancelAll()
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt
index 9222502f..9ca50e7a 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt
@@ -10,21 +10,23 @@ import androidx.compose.runtime.remember
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
+import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.ui.AppMaterialTheme
class MainActivity : ComponentActivity() {
private lateinit var sections: Map
private lateinit var navController: NavHostController
+ private lateinit var managerContext: RemoteSideContext
override fun onPostResume() {
super.onPostResume()
sections.values.forEach { it.onResumed() }
}
- override fun onNewIntent(intent: Intent?) {
+ override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
- intent?.getStringExtra("route")?.let { route ->
+ intent.getStringExtra("route")?.let { route ->
navController.popBackStack()
navController.navigate(route) {
popUpTo(navController.graph.findStartDestination().id){
@@ -38,7 +40,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME
- val managerContext = SharedContextHolder.remote(this).apply {
+ managerContext = SharedContextHolder.remote(this).apply {
activity = this@MainActivity
checkForRequirements()
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt
index 6f86b8a2..b6ed768a 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt
@@ -268,9 +268,9 @@ class FeaturesSection : Section() {
navController.navigate(FEATURE_CONTAINER_ROUTE.replace("{name}", property.name))
}
- if (container.globalState == null) return
+ if (!container.hasGlobalState) return
- var state by remember { mutableStateOf(container.globalState!!) }
+ var state by remember { mutableStateOf(container.globalState ?: false) }
Box(
modifier = Modifier
@@ -453,6 +453,22 @@ class FeaturesSection : Section() {
if (showSearchBar) return
var showExportDropdownMenu by remember { mutableStateOf(false) }
+ var showResetConfirmationDialog by remember { mutableStateOf(false) }
+
+ if (showResetConfirmationDialog) {
+ Dialog(onDismissRequest = { showResetConfirmationDialog = false }) {
+ alertDialogs.ConfirmDialog(
+ title = "Reset config",
+ message = "Are you sure you want to reset the config?",
+ onConfirm = {
+ context.config.reset()
+ context.shortToast("Config successfully reset!")
+ },
+ onDismiss = { showResetConfirmationDialog = false }
+ )
+ }
+ }
+
val actions = remember {
mapOf(
"Export" to {
@@ -477,10 +493,7 @@ class FeaturesSection : Section() {
}
}
},
- "Reset" to {
- context.config.reset()
- context.shortToast("Config successfully reset!")
- }
+ "Reset" to { showResetConfirmationDialog = true }
)
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt
index 79525c0b..4db58491 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt
@@ -14,6 +14,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -29,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -53,31 +55,34 @@ class AddFriendDialog(
private val section: SocialSection,
) {
@Composable
- private fun ListCardEntry(name: String, exists: Boolean, stateChanged: (state: Boolean) -> Unit = { }) {
- var state by remember { mutableStateOf(exists) }
+ private fun ListCardEntry(name: String, currentState: () -> Boolean, onState: (Boolean) -> Unit = {}) {
+ var currentState by remember { mutableStateOf(currentState()) }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
- state = !state
- stateChanged(state)
+ currentState = !currentState
+ onState(currentState)
}
.padding(4.dp),
- verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically
) {
Text(
text = name,
fontSize = 15.sp,
modifier = Modifier
.weight(1f)
+ .onGloballyPositioned {
+ currentState = currentState()
+ }
)
- androidx.compose.material3.Checkbox(
- checked = state,
+ Checkbox(
+ checked = currentState,
onCheckedChange = {
- state = it
- stateChanged(state)
+ currentState = it
+ onState(currentState)
}
)
}
@@ -227,13 +232,12 @@ class AddFriendDialog(
items(filteredGroups.size) {
val group = filteredGroups[it]
-
ListCardEntry(
name = group.name,
- exists = remember { context.modDatabase.getGroupInfo(group.conversationId) != null }
+ currentState = { context.modDatabase.getGroupInfo(group.conversationId) != null }
) { state ->
if (state) {
- context.bridgeService.triggerGroupSync(cachedGroups!![it].conversationId)
+ context.bridgeService.triggerGroupSync(group.conversationId)
} else {
context.modDatabase.deleteGroup(group.conversationId)
}
@@ -257,10 +261,10 @@ class AddFriendDialog(
ListCardEntry(
name = friend.displayName?.takeIf { name -> name.isNotBlank() } ?: friend.mutableUsername,
- exists = remember { context.modDatabase.getFriendInfo(friend.userId) != null }
+ currentState = { context.modDatabase.getFriendInfo(friend.userId) != null }
) { state ->
if (state) {
- context.bridgeService.triggerFriendSync(cachedFriends!![it].userId)
+ context.bridgeService.triggerFriendSync(friend.userId)
} else {
context.modDatabase.deleteFriend(friend.userId)
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt
index 55643c0b..b52f451c 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt
@@ -177,14 +177,11 @@ class ScopeContent(
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
- val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) ->
- if (selfieId == null || bitmojiId == null) return@let null
- BitmojiSelfie.getBitmojiSelfie(
- selfieId,
- bitmojiId,
- BitmojiSelfie.BitmojiSelfieType.THREE_D
- )
- }
+ val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(
+ friend.selfieId,
+ friend.bitmojiId,
+ BitmojiSelfie.BitmojiSelfieType.THREE_D
+ )
BitmojiImage(context = context, url = bitmojiUrl, size = 100)
Spacer(modifier = Modifier.height(16.dp))
Text(
@@ -218,13 +215,13 @@ class ScopeContent(
Column(
modifier = Modifier.weight(1f),
) {
- Text(text = "Count: ${streaks.length}", maxLines = 1)
+ Text(text = "Length: ${streaks.length}", maxLines = 1)
Text(text = "Expires in: ${computeStreakETA(streaks.expirationTimestamp)}", maxLines = 1)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
- Text(text = "Notify Expiration", maxLines = 1)
+ Text(text = "Reminder", maxLines = 1, modifier = Modifier.padding(end = 10.dp))
Switch(checked = shouldNotify, onCheckedChange = {
context.modDatabase.setFriendStreaksNotify(id, it)
shouldNotify = it
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt
index 6c59df29..2040639b 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt
@@ -18,7 +18,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.DeleteForever
import androidx.compose.material3.Card
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -36,6 +35,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -45,6 +46,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import kotlinx.coroutines.launch
+import me.rhunk.snapenhance.R
import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
import me.rhunk.snapenhance.core.messaging.SocialScope
@@ -175,24 +177,41 @@ class SocialSection : Section() {
}
SocialScope.FRIEND -> {
val friend = friendList[index]
+ val streaks = remember { context.modDatabase.getFriendStreaks(friend.userId) }
+
Row(
modifier = Modifier
.padding(10.dp)
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
- val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) ->
- if (selfieId == null || bitmojiId == null) return@let null
- BitmojiSelfie.getBitmojiSelfie(selfieId, bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
- }
- BitmojiImage(context = context, url = bitmojiUrl)
+ BitmojiImage(
+ context = context,
+ url = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
+ )
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
+ .weight(1f)
) {
Text(text = friend.displayName ?: friend.mutableUsername, maxLines = 1, fontWeight = FontWeight.Bold)
- Text(text = friend.userId, maxLines = 1, fontSize = 12.sp)
+ Text(text = friend.mutableUsername, maxLines = 1, fontSize = 12.sp, fontWeight = FontWeight.Light)
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (streaks != null && streaks.notify) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.streak_icon),
+ contentDescription = null,
+ modifier = Modifier.height(40.dp),
+ tint = if (streaks.isAboutToExpire())
+ MaterialTheme.colorScheme.error
+ else MaterialTheme.colorScheme.primary
+ )
+ Text(text = "${streaks.hoursLeft()}h", maxLines = 1, fontWeight = FontWeight.Bold)
+ }
}
}
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt
index 12667424..fd11427c 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt
@@ -28,9 +28,11 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.core.config.DataProcessors
import me.rhunk.snapenhance.core.config.PropertyPair
@@ -57,21 +59,22 @@ class AlertDialogs(
@Composable
fun ConfirmDialog(
title: String,
- data: String? = null,
+ message: String? = null,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
DefaultDialogCard {
Text(
text = title,
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier.padding(bottom = 10.dp)
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
)
- if (data != null) {
+ if (message != null) {
Text(
- text = data,
+ text = message,
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 10.dp)
+ modifier = Modifier.padding(bottom = 15.dp)
)
}
Row(
@@ -91,20 +94,21 @@ class AlertDialogs(
@Composable
fun InfoDialog(
title: String,
- data: String? = null,
+ message: String? = null,
onDismiss: () -> Unit,
) {
DefaultDialogCard {
Text(
text = title,
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier.padding(bottom = 10.dp)
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
)
- if (data != null) {
+ if (message != null) {
Text(
- text = data,
+ text = message,
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 10.dp)
+ modifier = Modifier.padding(bottom = 15.dp)
)
}
Row(
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt
index b61800e3..45f531d3 100644
--- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt
+++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt
@@ -36,18 +36,23 @@ fun BitmojiImage(context: RemoteSideContext, modifier: Modifier = Modifier, size
)
}
+fun ImageRequest.Builder.cacheKey(key: String?) = apply {
+ memoryCacheKey(key)
+ diskCacheKey(key)
+}
+
object ImageRequestHelper {
fun newBitmojiImageRequest(context: Context, url: String?) = ImageRequest.Builder(context)
.data(url)
.fallback(R.drawable.bitmoji_blank)
.precision(Precision.INEXACT)
.crossfade(true)
- .memoryCacheKey(url)
+ .cacheKey(url)
.build()
fun newDownloadPreviewImageRequest(context: Context, filePath: String?) = ImageRequest.Builder(context)
.data(filePath)
- .memoryCacheKey(filePath)
+ .cacheKey(filePath)
.crossfade(true)
.build()
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/streak_icon.xml b/app/src/main/res/drawable/streak_icon.xml
new file mode 100644
index 00000000..6bdc6fa9
--- /dev/null
+++ b/app/src/main/res/drawable/streak_icon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json
index aaa94a9a..07243586 100644
--- a/core/src/main/assets/lang/en_US.json
+++ b/core/src/main/assets/lang/en_US.json
@@ -309,6 +309,16 @@
}
}
},
+ "streaks_reminder": {
+ "name": "Streaks Reminder",
+ "description": "Reminds you to keep your streaks",
+ "properties": {
+ "interval": {
+ "name": "Interval",
+ "description": "The interval between each reminder (in hours)"
+ }
+ }
+ },
"experimental": {
"name": "Experimental",
"description": "Experimental features",
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt
index 4a219fdf..ded51434 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt
@@ -35,14 +35,14 @@ class BridgeClient(
fun start(callback: (Boolean) -> Unit) {
this.future = CompletableFuture()
+ //TODO: randomize package name
with(context.androidContext) {
//ensure the remote process is running
startActivity(Intent()
- .setClassName(BuildConfig.APPLICATION_ID, ForceStartActivity::class.java.name)
+ .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.ForceStartActivity")
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
)
- //TODO: randomize package name
val intent = Intent()
.setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.BridgeService")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt
deleted file mode 100644
index be6cca1f..00000000
--- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package me.rhunk.snapenhance.bridge
-
-import android.app.Activity
-import android.os.Bundle
-
-class ForceStartActivity : Activity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- finish()
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt
index c3b0eb91..d08b66eb 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt
@@ -6,10 +6,11 @@ import kotlin.reflect.KProperty
typealias ConfigParamsBuilder = ConfigParams.() -> Unit
open class ConfigContainer(
- var globalState: Boolean? = null
+ val hasGlobalState: Boolean = false
) {
var parentContainerKey: PropertyKey<*>? = null
val properties = mutableMapOf, PropertyValue<*>>()
+ var globalState: Boolean? = null
private inline fun registerProperty(
key: String,
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt
index 1ff51c0c..09daf353 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt
@@ -13,6 +13,8 @@ class Global : ConfigContainer() {
val disableGooglePlayDialogs = boolean("disable_google_play_dialogs")
val forceMediaSourceQuality = boolean("force_media_source_quality")
val betterNotifications = multiple("better_notifications", "snap", "chat", "reply_button", "download_button")
- val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray())
+ val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray()) {
+ customOptionTranslationPath = "features.options.notifications"
+ }
val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) }
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt
index 32697581..e82387fd 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt
@@ -16,7 +16,9 @@ class MessagingTweaks : ConfigContainer() {
"EXTERNAL_MEDIA",
"STICKER"
)
- val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray())
+ val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray()) {
+ customOptionTranslationPath = "features.options.notifications"
+ }
val messageLogger = boolean("message_logger") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) }
val galleryMediaSendOverride = boolean("gallery_media_send_override")
val messagePreviewLength = integer("message_preview_length", defaultValue = 20)
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt
index a0e69655..eb750174 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt
@@ -9,6 +9,7 @@ class RootConfig : ConfigContainer() {
val global = container("global", Global()) { icon = "MiscellaneousServices" }
val rules = container("rules", Rules()) { icon = "Rule" }
val camera = container("camera", Camera()) { icon = "Camera"}
+ val streaksReminder = container("streaks_reminder", StreaksReminderConfig()) { icon = "Alarm" }
val experimental = container("experimental", Experimental()) { icon = "Science" }
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" }
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt
index eab2e5bf..5e53ecef 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt
@@ -3,13 +3,13 @@ package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
class Spoof : ConfigContainer() {
- inner class Location : ConfigContainer(globalState = false) {
+ inner class Location : ConfigContainer(hasGlobalState = true) {
val latitude = float("location_latitude")
val longitude = float("location_longitude")
}
val location = container("location", Location())
- inner class Device : ConfigContainer(globalState = false) {
+ inner class Device : ConfigContainer(hasGlobalState = true) {
val fingerprint = string("device_fingerprint")
val androidId = string("device_android_id")
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/StreaksReminderConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/StreaksReminderConfig.kt
new file mode 100644
index 00000000..c8c50d9a
--- /dev/null
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/StreaksReminderConfig.kt
@@ -0,0 +1,7 @@
+package me.rhunk.snapenhance.core.config.impl
+
+import me.rhunk.snapenhance.core.config.ConfigContainer
+
+class StreaksReminderConfig : ConfigContainer(hasGlobalState = true) {
+ val interval = integer("interval", 2)
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt
index dbf7a939..e66c3d3e 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt
@@ -1,6 +1,7 @@
package me.rhunk.snapenhance.core.messaging
import me.rhunk.snapenhance.util.SerializableDataObject
+import kotlin.time.Duration.Companion.hours
enum class RuleState(
@@ -45,8 +46,16 @@ data class FriendStreaks(
val notify: Boolean,
val expirationTimestamp: Long,
val length: Int
-) : SerializableDataObject()
+) : SerializableDataObject() {
+ companion object {
+ //TODO: config
+ val EXPIRE_THRESHOLD = 12.hours
+ }
+ fun hoursLeft() = (expirationTimestamp - System.currentTimeMillis()) / 1000 / 60 / 60
+
+ fun isAboutToExpire() = expirationTimestamp - System.currentTimeMillis() < EXPIRE_THRESHOLD.inWholeMilliseconds
+}
data class MessagingGroupInfo(
val conversationId: String,
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt
index 43cc30e9..288ab333 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt
@@ -61,11 +61,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
): DownloadManagerClient {
val generatedHash = mediaIdentifier.hashCode().toString(16).replaceFirst("-", "")
- val iconUrl = friendInfo?.takeIf {
- it.bitmojiAvatarId != null && it.bitmojiSelfieId != null
- }?.let {
- BitmojiSelfie.getBitmojiSelfie(it.bitmojiSelfieId!!, it.bitmojiAvatarId!!, BitmojiSelfie.BitmojiSelfieType.THREE_D)
- }
+ val iconUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo?.bitmojiSelfieId, friendInfo?.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
val downloadLogging by context.config.downloader.logging
if (downloadLogging.contains("started")) {
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt
index 3f6cdd31..4de45c21 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt
@@ -50,7 +50,7 @@ object ViewAppearanceHelper {
}
}
- val snapchatFontResId = resources.getIdentifier("avenir_next_medium", "font", "com.snapchat.android")
+ val snapchatFontResId = resources.getIdentifier("avenir_next_medium", "font", Constants.SNAPCHAT_PACKAGE_NAME)
val scalingFactor = resources.displayMetrics.densityDpi.toDouble() / 400
with(component) {
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt
index ee0538bb..66537761 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt
@@ -53,7 +53,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
profile.bitmojiSelfieId.toString(),
profile.bitmojiAvatarId.toString(),
BitmojiSelfie.BitmojiSelfieType.THREE_D
- )
+ )!!
)
}
} catch (e: Throwable) {
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt
index 3e5a1fa0..7a0db782 100644
--- a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt
+++ b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt
@@ -6,7 +6,10 @@ object BitmojiSelfie {
THREE_D
}
- fun getBitmojiSelfie(selfieId: String, avatarId: String, type: BitmojiSelfieType): String {
+ fun getBitmojiSelfie(selfieId: String?, avatarId: String?, type: BitmojiSelfieType): String? {
+ if (selfieId.isNullOrEmpty() || avatarId.isNullOrEmpty()) {
+ return null
+ }
return when (type) {
BitmojiSelfieType.STANDARD -> "https://sdk.bitmoji.com/render/panel/$selfieId-$avatarId-v1.webp?transparent=1"
BitmojiSelfieType.THREE_D -> "https://images.bitmoji.com/3d/render/$selfieId-$avatarId-v1.webp?trim=circle"