feat: scope tab base logic

This commit is contained in:
rhunk
2023-08-19 20:09:35 +02:00
parent 2db332c3cd
commit 6cabb92c04
10 changed files with 331 additions and 141 deletions

View File

@ -12,7 +12,7 @@ import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper
import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
import me.rhunk.snapenhance.core.messaging.RuleScope
import me.rhunk.snapenhance.core.messaging.MessagingScope
import me.rhunk.snapenhance.database.objects.FriendInfo
import me.rhunk.snapenhance.download.DownloadProcessor
import me.rhunk.snapenhance.util.SerializableDataObject
@ -112,7 +112,7 @@ class BridgeService : Service() {
}
override fun getRules(objectType: String, uuid: String): MutableList<String> {
remoteSideContext.modDatabase.getRulesFromId(RuleScope.valueOf(objectType), uuid)
remoteSideContext.modDatabase.getRulesFromId(MessagingScope.valueOf(objectType), uuid)
.let { rules ->
return rules.map { it.toJson() }.toMutableList()
}
@ -122,14 +122,14 @@ class BridgeService : Service() {
Logger.debug("Syncing remote")
syncCallback = callback
measureTimeMillis {
remoteSideContext.modDatabase.getFriendsIds().forEach { friendId ->
remoteSideContext.modDatabase.getFriends().map { it.userId } .forEach { friendId ->
runCatching {
triggerFriendSync(friendId)
}.onFailure {
Logger.error("Failed to sync friend $friendId", it)
}
}
remoteSideContext.modDatabase.getGroupsIds().forEach { groupId ->
remoteSideContext.modDatabase.getGroups().map { it.conversationId }.forEach { groupId ->
runCatching {
triggerGroupSync(groupId)
}.onFailure {

View File

@ -8,7 +8,7 @@ import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
import me.rhunk.snapenhance.core.messaging.MessagingRule
import me.rhunk.snapenhance.core.messaging.Mode
import me.rhunk.snapenhance.core.messaging.RuleScope
import me.rhunk.snapenhance.core.messaging.MessagingScope
import me.rhunk.snapenhance.database.objects.FriendInfo
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
import me.rhunk.snapenhance.util.ktx.getInteger
@ -33,14 +33,16 @@ class ModDatabase(
database = context.androidContext.openOrCreateDatabase("main.db", 0, null)
SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf(
"friends" to listOf(
"userId VARCHAR PRIMARY KEY",
"id INTEGER PRIMARY KEY AUTOINCREMENT",
"userId VARCHAR UNIQUE",
"displayName VARCHAR",
"mutableUsername VARCHAR",
"bitmojiId VARCHAR",
"selfieId VARCHAR"
),
"groups" to listOf(
"conversationId VARCHAR PRIMARY KEY",
"id INTEGER PRIMARY KEY AUTOINCREMENT",
"conversationId VARCHAR UNIQUE",
"name VARCHAR",
"participantsCount INTEGER"
),
@ -73,26 +75,6 @@ class ModDatabase(
))
}
fun getFriendsIds(): List<String> {
return database.rawQuery("SELECT userId FROM friends", null).use { cursor ->
val ids = mutableListOf<String>()
while (cursor.moveToNext()) {
ids.add(cursor.getString(0))
}
ids
}
}
fun getGroupsIds(): List<String> {
return database.rawQuery("SELECT conversationId FROM groups", null).use { cursor ->
val ids = mutableListOf<String>()
while (cursor.moveToNext()) {
ids.add(cursor.getString(0))
}
ids
}
}
fun getGroups(): List<MessagingGroupInfo> {
return database.rawQuery("SELECT * FROM groups", null).use { cursor ->
val groups = mutableListOf<MessagingGroupInfo>()
@ -107,8 +89,8 @@ class ModDatabase(
}
}
fun getFriends(): List<MessagingFriendInfo> {
return database.rawQuery("SELECT * FROM friends", null).use { cursor ->
fun getFriends(descOrder: Boolean = false): List<MessagingFriendInfo> {
return database.rawQuery("SELECT * FROM friends ORDER BY id ${if (descOrder) "DESC" else "ASC"}", null).use { cursor ->
val friends = mutableListOf<MessagingFriendInfo>()
while (cursor.moveToNext()) {
runCatching {
@ -131,7 +113,7 @@ class ModDatabase(
fun syncGroupInfo(conversationInfo: MessagingGroupInfo) {
executeAsync {
try {
database.execSQL("INSERT OR REPLACE INTO groups VALUES (?, ?, ?)", arrayOf(
database.execSQL("INSERT OR REPLACE INTO groups (conversationId, name, participantsCount) VALUES (?, ?, ?)", arrayOf(
conversationInfo.conversationId,
conversationInfo.name,
conversationInfo.participantsCount
@ -145,13 +127,16 @@ class ModDatabase(
fun syncFriend(friend: FriendInfo) {
executeAsync {
try {
database.execSQL("INSERT OR REPLACE INTO friends VALUES (?, ?, ?, ?, ?)", arrayOf(
database.execSQL(
"INSERT OR REPLACE INTO friends (userId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?)",
arrayOf(
friend.userId,
friend.displayName,
friend.usernameForSorting!!,
friend.bitmojiAvatarId,
friend.bitmojiSelfieId
))
)
)
//sync streaks
if (friend.streakLength > 0) {
database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, count) VALUES (?, ?, ?)", arrayOf(
@ -168,13 +153,13 @@ class ModDatabase(
}
}
fun getRulesFromId(type: RuleScope, targetUuid: String): List<MessagingRule> {
fun getRulesFromId(type: MessagingScope, targetUuid: String): List<MessagingRule> {
return database.rawQuery("SELECT * FROM rules WHERE objectType = ? AND targetUuid = ?", arrayOf(type.name, targetUuid)).use { cursor ->
val rules = mutableListOf<MessagingRule>()
while (cursor.moveToNext()) {
rules.add(MessagingRule(
id = cursor.getInteger("id"),
ruleScope = RuleScope.valueOf(cursor.getStringOrNull("scope")!!),
messagingScope = MessagingScope.valueOf(cursor.getStringOrNull("scope")!!),
targetUuid = cursor.getStringOrNull("targetUuid")!!,
enabled = cursor.getInteger("enabled") == 1,
mode = Mode.valueOf(cursor.getStringOrNull("mode")!!),
@ -185,6 +170,42 @@ class ModDatabase(
}
}
fun getFriendInfo(userId: String): MessagingFriendInfo? {
return database.rawQuery("SELECT * FROM friends WHERE userId = ?", arrayOf(userId)).use { cursor ->
if (!cursor.moveToFirst()) return@use null
MessagingFriendInfo(
userId = cursor.getStringOrNull("userId")!!,
displayName = cursor.getStringOrNull("displayName"),
mutableUsername = cursor.getStringOrNull("mutableUsername")!!,
bitmojiId = cursor.getStringOrNull("bitmojiId"),
selfieId = cursor.getStringOrNull("selfieId")
)
}
}
fun deleteFriend(userId: String) {
executeAsync {
database.execSQL("DELETE FROM friends WHERE userId = ?", arrayOf(userId))
}
}
fun deleteGroup(conversationId: String) {
executeAsync {
database.execSQL("DELETE FROM groups WHERE conversationId = ?", arrayOf(conversationId))
}
}
fun getGroupInfo(conversationId: String): MessagingGroupInfo? {
return database.rawQuery("SELECT * FROM groups WHERE conversationId = ?", arrayOf(conversationId)).use { cursor ->
if (!cursor.moveToFirst()) return@use null
MessagingGroupInfo(
conversationId = cursor.getStringOrNull("conversationId")!!,
name = cursor.getStringOrNull("name")!!,
participantsCount = cursor.getInteger("participantsCount")
)
}
}
fun getFriendStreaks(userId: String): FriendStreaks? {
return database.rawQuery("SELECT * FROM streaks WHERE userId = ?", arrayOf(userId)).use { cursor ->
if (!cursor.moveToFirst()) return@use null

View File

@ -1,8 +1,12 @@
package me.rhunk.snapenhance.ui.manager
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
@ -16,7 +20,10 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
@ -63,8 +70,19 @@ class Navigation(
TopAppBar(title = {
Text(text = currentSection.sectionTopBarName())
}, navigationIcon = {
if (currentSection.canGoBack()) {
IconButton(onClick = { navHostController.popBackStack() }) {
val backButtonAnimation by animateFloatAsState(if (currentSection.canGoBack()) 1f else 0f,
label = "backButtonAnimation"
)
Box(
modifier = Modifier
.graphicsLayer { alpha = backButtonAnimation }
.width(lerp(0.dp, 48.dp, backButtonAnimation))
.height(48.dp)
) {
IconButton(
onClick = { navHostController.popBackStack() }
) {
Icon(Icons.Filled.ArrowBack, contentDescription = null)
}
}

View File

@ -42,7 +42,9 @@ class AddFriendDialog(
@Composable
private fun ListCardEntry(name: String, modifier: Modifier = Modifier) {
Card(
modifier = Modifier.padding(5.dp).then(modifier),
modifier = Modifier
.padding(5.dp)
.then(modifier),
) {
Text(text = name, modifier = Modifier.padding(10.dp))
}
@ -86,9 +88,10 @@ class AddFriendDialog(
timeoutJob?.cancel()
dismiss()
}) {
Card {
if (hasFetchError) {
Text(text = "Failed to load friends and groups. Make sure Snapchat is installed and logged in.")
return@Dialog
return@Card
}
if (cachedGroups == null || cachedFriends == null) {
CircularProgressIndicator(
@ -98,7 +101,7 @@ class AddFriendDialog(
strokeWidth = 3.dp,
color = MaterialTheme.colorScheme.onPrimary
)
return@Dialog
return@Card
}
LazyColumn(
@ -131,4 +134,5 @@ class AddFriendDialog(
}
}
}
}
}

View File

@ -1,4 +0,0 @@
package me.rhunk.snapenhance.ui.manager.sections.social
class FriendTab {
}

View File

@ -0,0 +1,72 @@
package me.rhunk.snapenhance.ui.manager.sections.social
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import me.rhunk.snapenhance.RemoteSideContext
class ScopeTab(
private val context: RemoteSideContext,
private val section: SocialSection,
private val navController: NavController,
private val id: String
) {
@Composable
fun Friend() {
//fetch the friend from the database
val friend = remember { context.modDatabase.getFriendInfo(id) } ?: run {
Text(text = "Friend not found")
return
}
Column {
Text(text = friend.displayName ?: "No display name", maxLines = 1)
Text(text = "bitmojiId: ${friend.bitmojiId ?: "No bitmojiId"}", maxLines = 1)
Text(text = "selfieId: ${friend.selfieId ?: "No selfieId"}", maxLines = 1)
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(onClick = {
context.modDatabase.deleteFriend(id)
section.onResumed()
navController.popBackStack()
}) {
Text(text = "Delete friend")
}
}
}
@Composable
fun Group() {
//fetch the group from the database
val group = remember { context.modDatabase.getGroupInfo(id) } ?: run {
Text(text = "Group not found")
return
}
Column {
Text(text = group.name, maxLines = 1)
Text(text = "participantsCount: ${group.participantsCount}", maxLines = 1)
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(onClick = {
context.modDatabase.deleteGroup(id)
section.onResumed()
navController.popBackStack()
}) {
Text(text = "Delete group")
}
}
}
}

View File

@ -1,14 +1,22 @@
package me.rhunk.snapenhance.ui.manager.sections.social
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -18,6 +26,7 @@ import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -26,6 +35,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
@ -36,15 +48,48 @@ class SocialSection : Section() {
private lateinit var friendList: List<MessagingFriendInfo>
private lateinit var groupList: List<MessagingGroupInfo>
companion object {
const val MAIN_ROUTE = "social_route"
const val FRIEND_INFO_ROUTE = "friend_info/{id}"
const val GROUP_INFO_ROUTE = "group_info/{id}"
}
private var currentScopeTab: ScopeTab? = null
private val addFriendDialog by lazy {
AddFriendDialog(context, this)
}
//FIXME: don't reload the entire list when a friend is added/deleted
override fun onResumed() {
friendList = context.modDatabase.getFriends()
friendList = context.modDatabase.getFriends(descOrder = true)
groupList = context.modDatabase.getGroups()
}
override fun canGoBack() = navController.currentBackStackEntry?.destination?.route != MAIN_ROUTE
override fun build(navGraphBuilder: NavGraphBuilder) {
fun switchTab(id: String) = ScopeTab(context, this, navController, id).also { tab ->
currentScopeTab = tab
}
navGraphBuilder.navigation(route = enumSection.route, startDestination = MAIN_ROUTE) {
composable(MAIN_ROUTE) {
Content()
}
composable(FRIEND_INFO_ROUTE) {
val id = it.arguments?.getString("id") ?: return@composable
remember { switchTab(id) }.Friend()
}
composable(GROUP_INFO_ROUTE) {
val id = it.arguments?.getString("id") ?: return@composable
remember { switchTab(id) }.Group()
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -100,23 +145,55 @@ class SocialSection : Section() {
}
}
HorizontalPager(modifier = Modifier.padding(paddingValues), state = pagerState) { page ->
Column(
modifier = Modifier.fillMaxSize(),
) {
when (page) {
0 -> {
Text(text = "Friends")
Column {
friendList.forEach {
Text(text = it.displayName ?: it.mutableUsername)
LazyColumn(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
) {
if (friendList.isEmpty()) {
item {
Text(text = "No friends found")
}
}
items(friendList.size) { index ->
val friend = friendList[index]
Card(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.height(100.dp)
.clickable {
navController.navigate(
FRIEND_INFO_ROUTE.replace(
"{id}",
friend.userId
)
)
},
) {
Text(text = friend.displayName ?: friend.mutableUsername)
}
}
}
}
1 -> {
Text(text = "Groups")
Column {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
.scrollable(rememberScrollState(), Orientation.Vertical)
) {
groupList.forEach {
Card(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.height(100.dp),
) {
Text(text = it.name)
}
}

View File

@ -130,6 +130,7 @@ class SnapEnhance {
private fun syncRemote() {
val database = appContext.database
appContext.executeAsync {
appContext.bridgeClient.sync(object : SyncCallback.Stub() {
override fun syncFriend(uuid: String): String? {
return database.getFriendInfo(uuid)?.toJson()
@ -175,4 +176,5 @@ class SnapEnhance {
)
}
}
}
}

View File

@ -16,7 +16,7 @@ import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.types.FileActionType
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.messaging.MessagingRule
import me.rhunk.snapenhance.core.messaging.RuleScope
import me.rhunk.snapenhance.core.messaging.MessagingScope
import me.rhunk.snapenhance.data.LocalePair
import me.rhunk.snapenhance.util.SerializableDataObject
import java.util.concurrent.CompletableFuture
@ -136,7 +136,7 @@ class BridgeClient(
fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends)
fun getRulesFromId(type: RuleScope, targetUuid: String): List<MessagingRule> {
fun getRulesFromId(type: MessagingScope, targetUuid: String): List<MessagingRule> {
return service.getRules(type.name, targetUuid).map {
SerializableDataObject.fromJson(it, MessagingRule::class.java)
}.toList()

View File

@ -8,18 +8,18 @@ enum class Mode {
WHITELIST
}
enum class RuleScope {
enum class MessagingScope {
FRIEND,
GROUP
}
enum class ConversationFeature(
val value: String,
val ruleScope: RuleScope,
val messagingScope: MessagingScope,
) {
DOWNLOAD("download", RuleScope.FRIEND),
STEALTH("stealth", RuleScope.GROUP),
AUTO_SAVE("auto_save", RuleScope.GROUP);
DOWNLOAD("download", MessagingScope.FRIEND),
STEALTH("stealth", MessagingScope.GROUP),
AUTO_SAVE("auto_save", MessagingScope.GROUP);
}
data class FriendStreaks(
@ -47,7 +47,7 @@ data class MessagingFriendInfo(
data class MessagingRule(
val id: Int,
val ruleScope: RuleScope,
val messagingScope: MessagingScope,
val targetUuid: String,
val enabled: Boolean,
val mode: Mode?,