mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-11 20:04:32 +02:00
feat(manager): conversation preview (wip)
- add messaging bridge - refactor export chat messages
This commit is contained in:
parent
2b0e4ad09a
commit
3e9c97c18c
@ -48,7 +48,7 @@ class RemoteSideContext(
|
|||||||
val coroutineScope = CoroutineScope(Dispatchers.IO)
|
val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private var _activity: WeakReference<ComponentActivity>? = null
|
private var _activity: WeakReference<ComponentActivity>? = null
|
||||||
lateinit var bridgeService: BridgeService
|
var bridgeService: BridgeService? = null
|
||||||
|
|
||||||
var activity: ComponentActivity?
|
var activity: ComponentActivity?
|
||||||
get() = _activity?.get()
|
get() = _activity?.get()
|
||||||
@ -158,11 +158,13 @@ class RemoteSideContext(
|
|||||||
log.debug(message.toString())
|
log.debug(message.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasMessagingBridge() = bridgeService != null && bridgeService?.messagingBridge != null
|
||||||
|
|
||||||
fun checkForRequirements(overrideRequirements: Int? = null): Boolean {
|
fun checkForRequirements(overrideRequirements: Int? = null): Boolean {
|
||||||
var requirements = overrideRequirements ?: 0
|
var requirements = overrideRequirements ?: 0
|
||||||
|
|
||||||
if(BuildConfig.DEBUG) {
|
if(BuildConfig.DEBUG) {
|
||||||
var unixTime = System.currentTimeMillis() / 1000 //unix time in seconds cuz cool
|
val unixTime = System.currentTimeMillis() / 1000 //unix time in seconds cuz cool
|
||||||
if(BuildConfig.BUILD_DATE + 604800 < unixTime.toInt()) {
|
if(BuildConfig.BUILD_DATE + 604800 < unixTime.toInt()) {
|
||||||
Toast.makeText(androidContext, "This SnapEnhance build has expired.", Toast.LENGTH_LONG).show();
|
Toast.makeText(androidContext, "This SnapEnhance build has expired.", Toast.LENGTH_LONG).show();
|
||||||
throw RuntimeException("This build has expired. This crash is intentional.")
|
throw RuntimeException("This build has expired. This crash is intentional.")
|
||||||
|
@ -5,6 +5,7 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.SharedContextHolder
|
import me.rhunk.snapenhance.SharedContextHolder
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
||||||
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
|
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
|
||||||
import me.rhunk.snapenhance.common.bridge.types.FileActionType
|
import me.rhunk.snapenhance.common.bridge.types.FileActionType
|
||||||
import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper
|
import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper
|
||||||
@ -22,6 +23,11 @@ class BridgeService : Service() {
|
|||||||
private lateinit var messageLoggerWrapper: MessageLoggerWrapper
|
private lateinit var messageLoggerWrapper: MessageLoggerWrapper
|
||||||
private lateinit var remoteSideContext: RemoteSideContext
|
private lateinit var remoteSideContext: RemoteSideContext
|
||||||
lateinit var syncCallback: SyncCallback
|
lateinit var syncCallback: SyncCallback
|
||||||
|
var messagingBridge: MessagingBridge? = null
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
remoteSideContext.bridgeService = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
remoteSideContext = SharedContextHolder.remote(this).apply {
|
remoteSideContext = SharedContextHolder.remote(this).apply {
|
||||||
@ -177,6 +183,9 @@ class BridgeService : Service() {
|
|||||||
|
|
||||||
override fun getE2eeInterface() = remoteSideContext.e2eeImplementation
|
override fun getE2eeInterface() = remoteSideContext.e2eeImplementation
|
||||||
override fun getMessageLogger() = messageLoggerWrapper
|
override fun getMessageLogger() = messageLoggerWrapper
|
||||||
|
override fun registerMessagingBridge(bridge: MessagingBridge) {
|
||||||
|
messagingBridge = bridge
|
||||||
|
}
|
||||||
|
|
||||||
override fun openSettingsOverlay() {
|
override fun openSettingsOverlay() {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
@ -220,7 +220,7 @@ class AddFriendDialog(
|
|||||||
getCurrentState = { context.modDatabase.getGroupInfo(group.conversationId) != null }
|
getCurrentState = { context.modDatabase.getGroupInfo(group.conversationId) != null }
|
||||||
) { state ->
|
) { state ->
|
||||||
if (state) {
|
if (state) {
|
||||||
context.bridgeService.triggerScopeSync(SocialScope.GROUP, group.conversationId)
|
context.bridgeService?.triggerScopeSync(SocialScope.GROUP, group.conversationId)
|
||||||
} else {
|
} else {
|
||||||
context.modDatabase.deleteGroup(group.conversationId)
|
context.modDatabase.deleteGroup(group.conversationId)
|
||||||
}
|
}
|
||||||
@ -247,7 +247,7 @@ class AddFriendDialog(
|
|||||||
getCurrentState = { context.modDatabase.getFriendInfo(friend.userId) != null }
|
getCurrentState = { context.modDatabase.getFriendInfo(friend.userId) != null }
|
||||||
) { state ->
|
) { state ->
|
||||||
if (state) {
|
if (state) {
|
||||||
context.bridgeService.triggerScopeSync(SocialScope.FRIEND, friend.userId)
|
context.bridgeService?.triggerScopeSync(SocialScope.FRIEND, friend.userId)
|
||||||
} else {
|
} else {
|
||||||
context.modDatabase.deleteFriend(friend.userId)
|
context.modDatabase.deleteFriend(friend.userId)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.manager.sections.social
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.types.Message
|
||||||
|
import me.rhunk.snapenhance.common.data.ContentType
|
||||||
|
import me.rhunk.snapenhance.common.data.SocialScope
|
||||||
|
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||||
|
import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||||
|
|
||||||
|
class MessagingPreview(
|
||||||
|
private val context: RemoteSideContext,
|
||||||
|
private val scope: SocialScope,
|
||||||
|
private val scopeId: String
|
||||||
|
) {
|
||||||
|
private lateinit var coroutineScope: CoroutineScope
|
||||||
|
private lateinit var messagingBridge: MessagingBridge
|
||||||
|
private lateinit var previewScrollState: LazyListState
|
||||||
|
private var conversationId: String? = null
|
||||||
|
private val messages = sortedMapOf<Long, Message>()
|
||||||
|
private var messageSize by mutableIntStateOf(0)
|
||||||
|
private var lastMessageId = Long.MAX_VALUE
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ConversationPreview() {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
state = previewScrollState,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(40.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text("No messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (messages.size > 0) {
|
||||||
|
fetchNewMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(messageSize) {index ->
|
||||||
|
val messageReader = ProtoReader(messages.entries.elementAt(index).value.content)
|
||||||
|
val contentType = ContentType.fromMessageContainer(messageReader)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(5.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(5.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text("[$contentType] ${messageReader.getString(2, 1) ?: ""}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchNewMessages() {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) cs@{
|
||||||
|
runCatching {
|
||||||
|
val queriedMessages = messagingBridge.fetchConversationWithMessagesPaginated(
|
||||||
|
conversationId!!,
|
||||||
|
100,
|
||||||
|
lastMessageId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (queriedMessages == null) {
|
||||||
|
context.shortToast("Failed to fetch messages")
|
||||||
|
return@cs
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
messages.putAll(queriedMessages.map { it.serverMessageId to it })
|
||||||
|
messageSize = messages.size
|
||||||
|
if (queriedMessages.isNotEmpty()) {
|
||||||
|
lastMessageId = queriedMessages.first().clientMessageId
|
||||||
|
previewScrollState.scrollToItem(queriedMessages.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
context.shortToast("Failed to fetch messages: ${it.message}")
|
||||||
|
}
|
||||||
|
context.log.verbose("fetched ${messages.size} messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMessagingBridgeReady() {
|
||||||
|
messagingBridge = context.bridgeService!!.messagingBridge!!
|
||||||
|
conversationId = if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId
|
||||||
|
if (conversationId == null) {
|
||||||
|
context.longToast("Failed to fetch conversation id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchNewMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoadingRow() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(40.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding()
|
||||||
|
.size(30.dp),
|
||||||
|
strokeWidth = 3.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Content() {
|
||||||
|
previewScrollState = rememberLazyListState()
|
||||||
|
coroutineScope = rememberCoroutineScope()
|
||||||
|
var isBridgeConnected by remember { mutableStateOf(false) }
|
||||||
|
var hasBridgeError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
isBridgeConnected = context.hasMessagingBridge()
|
||||||
|
if (isBridgeConnected) {
|
||||||
|
onMessagingBridgeReady()
|
||||||
|
} else {
|
||||||
|
SnapWidgetBroadcastReceiverHelper.create("wakeup") {}.also {
|
||||||
|
context.androidContext.sendBroadcast(it)
|
||||||
|
}
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
withTimeout(10000) {
|
||||||
|
while (!context.hasMessagingBridge()) {
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
isBridgeConnected = true
|
||||||
|
onMessagingBridgeReady()
|
||||||
|
}
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
if (it != null) {
|
||||||
|
hasBridgeError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBridgeError) {
|
||||||
|
Text("Failed to connect to Snapchat through bridge service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBridgeConnected && !hasBridgeError) {
|
||||||
|
LoadingRow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBridgeConnected && !hasBridgeError) {
|
||||||
|
ConversationPreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.pager.HorizontalPager
|
|||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.RemoveRedEye
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.DeleteForever
|
import androidx.compose.material.icons.rounded.DeleteForever
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@ -42,8 +43,7 @@ class SocialSection : Section() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAIN_ROUTE = "social_route"
|
const val MAIN_ROUTE = "social_route"
|
||||||
const val FRIEND_INFO_ROUTE = "friend_info/{id}"
|
const val MESSAGING_PREVIEW_ROUTE = "messaging_preview/?id={id}&scope={scope}"
|
||||||
const val GROUP_INFO_ROUTE = "group_info/{id}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentScopeContent: ScopeContent? = null
|
private var currentScopeContent: ScopeContent? = null
|
||||||
@ -70,12 +70,26 @@ class SocialSection : Section() {
|
|||||||
composable(scope.tabRoute) {
|
composable(scope.tabRoute) {
|
||||||
val id = it.arguments?.getString("id") ?: return@composable
|
val id = it.arguments?.getString("id") ?: return@composable
|
||||||
remember {
|
remember {
|
||||||
ScopeContent(context, this@SocialSection, navController, scope, id).also { tab ->
|
ScopeContent(
|
||||||
|
context,
|
||||||
|
this@SocialSection,
|
||||||
|
navController,
|
||||||
|
scope,
|
||||||
|
id
|
||||||
|
).also { tab ->
|
||||||
currentScopeContent = tab
|
currentScopeContent = tab
|
||||||
}
|
}
|
||||||
}.Content()
|
}.Content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(MESSAGING_PREVIEW_ROUTE) {
|
||||||
|
val id = it.arguments?.getString("id") ?: return@composable
|
||||||
|
val scope = it.arguments?.getString("scope") ?: return@composable
|
||||||
|
remember {
|
||||||
|
MessagingPreview(context, SocialScope.getByName(scope), id)
|
||||||
|
}.Content()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,13 +104,15 @@ class SocialSection : Section() {
|
|||||||
remember { AlertDialogs(context.translation) }.ConfirmDialog(
|
remember { AlertDialogs(context.translation) }.ConfirmDialog(
|
||||||
title = "Are you sure you want to delete this ${scopeContent.scope.key.lowercase()}?",
|
title = "Are you sure you want to delete this ${scopeContent.scope.key.lowercase()}?",
|
||||||
onDismiss = { deleteConfirmDialog = false },
|
onDismiss = { deleteConfirmDialog = false },
|
||||||
onConfirm = { scopeContent.deleteScope(coroutineScope); deleteConfirmDialog = false }
|
onConfirm = {
|
||||||
|
scopeContent.deleteScope(coroutineScope); deleteConfirmDialog = false
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentRoute != MAIN_ROUTE) {
|
if (currentRoute == SocialScope.FRIEND.tabRoute || currentRoute == SocialScope.GROUP.tabRoute) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { deleteConfirmDialog = true },
|
onClick = { deleteConfirmDialog = true },
|
||||||
) {
|
) {
|
||||||
@ -128,7 +144,11 @@ class SocialSection : Section() {
|
|||||||
|
|
||||||
if (listSize == 0) {
|
if (listSize == 0) {
|
||||||
item {
|
item {
|
||||||
Text(text = "(empty)", modifier = Modifier.fillMaxWidth().padding(10.dp), textAlign = TextAlign.Center)
|
Text(
|
||||||
|
text = "(empty)", modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp), textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,31 +169,41 @@ class SocialSection : Section() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
when (scope) {
|
Row(
|
||||||
SocialScope.GROUP -> {
|
modifier = Modifier
|
||||||
val group = groupList[index]
|
.padding(10.dp)
|
||||||
Column(
|
.fillMaxSize(),
|
||||||
modifier = Modifier
|
verticalAlignment = Alignment.CenterVertically
|
||||||
.padding(10.dp)
|
) {
|
||||||
.fillMaxSize(),
|
when (scope) {
|
||||||
verticalArrangement = Arrangement.Center
|
SocialScope.GROUP -> {
|
||||||
) {
|
val group = groupList[index]
|
||||||
Text(text = group.name, maxLines = 1, fontWeight = FontWeight.Bold)
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = group.name,
|
||||||
|
maxLines = 1,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
SocialScope.FRIEND -> {
|
|
||||||
val friend = friendList[index]
|
|
||||||
val streaks = remember { context.modDatabase.getFriendStreaks(friend.userId) }
|
|
||||||
|
|
||||||
Row(
|
SocialScope.FRIEND -> {
|
||||||
modifier = Modifier
|
val friend = friendList[index]
|
||||||
.padding(10.dp)
|
val streaks =
|
||||||
.fillMaxSize(),
|
remember { context.modDatabase.getFriendStreaks(friend.userId) }
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
BitmojiImage(
|
BitmojiImage(
|
||||||
context = context,
|
context = context,
|
||||||
url = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
|
url = BitmojiSelfie.getBitmojiSelfie(
|
||||||
|
friend.selfieId,
|
||||||
|
friend.bitmojiId,
|
||||||
|
BitmojiSelfie.BitmojiSelfieType.THREE_D
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -181,8 +211,17 @@ class SocialSection : Section() {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(text = friend.displayName ?: friend.mutableUsername, maxLines = 1, fontWeight = FontWeight.Bold)
|
Text(
|
||||||
Text(text = friend.mutableUsername, maxLines = 1, fontSize = 12.sp, fontWeight = FontWeight.Light)
|
text = friend.displayName ?: friend.mutableUsername,
|
||||||
|
maxLines = 1,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = friend.mutableUsername,
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@ -197,7 +236,11 @@ class SocialSection : Section() {
|
|||||||
else MaterialTheme.colorScheme.primary
|
else MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = context.translation.format("manager.sections.social.streaks_expiration_short", "hours" to ((streaks.expirationTimestamp - System.currentTimeMillis()) / 3600000).toInt().toString()),
|
text = context.translation.format(
|
||||||
|
"manager.sections.social.streaks_expiration_short",
|
||||||
|
"hours" to ((streaks.expirationTimestamp - System.currentTimeMillis()) / 3600000).toInt()
|
||||||
|
.toString()
|
||||||
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@ -205,6 +248,17 @@ class SocialSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilledIconButton(onClick = {
|
||||||
|
navController.navigate(
|
||||||
|
MESSAGING_PREVIEW_ROUTE.replace("{id}", id).replace("{scope}", scope.key)
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.RemoveRedEye,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,15 +312,24 @@ class SocialSection : Section() {
|
|||||||
selected = pagerState.currentPage == index,
|
selected = pagerState.currentPage == index,
|
||||||
onClick = {
|
onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
pagerState.animateScrollToPage( index )
|
pagerState.animateScrollToPage(index)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
|
text = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(modifier = Modifier.padding(paddingValues), state = pagerState) { page ->
|
HorizontalPager(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
state = pagerState
|
||||||
|
) { page ->
|
||||||
when (page) {
|
when (page) {
|
||||||
0 -> ScopeList(SocialScope.FRIEND)
|
0 -> ScopeList(SocialScope.FRIEND)
|
||||||
1 -> ScopeList(SocialScope.GROUP)
|
1 -> ScopeList(SocialScope.GROUP)
|
||||||
|
@ -7,6 +7,7 @@ import me.rhunk.snapenhance.bridge.scripting.IScripting;
|
|||||||
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface;
|
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface;
|
||||||
import me.rhunk.snapenhance.bridge.MessageLoggerInterface;
|
import me.rhunk.snapenhance.bridge.MessageLoggerInterface;
|
||||||
import me.rhunk.snapenhance.bridge.ConfigStateListener;
|
import me.rhunk.snapenhance.bridge.ConfigStateListener;
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge;
|
||||||
|
|
||||||
interface BridgeInterface {
|
interface BridgeInterface {
|
||||||
/**
|
/**
|
||||||
@ -80,6 +81,8 @@ interface BridgeInterface {
|
|||||||
|
|
||||||
MessageLoggerInterface getMessageLogger();
|
MessageLoggerInterface getMessageLogger();
|
||||||
|
|
||||||
|
void registerMessagingBridge(MessagingBridge bridge);
|
||||||
|
|
||||||
void openSettingsOverlay();
|
void openSettingsOverlay();
|
||||||
|
|
||||||
void closeSettingsOverlay();
|
void closeSettingsOverlay();
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package me.rhunk.snapenhance.bridge.snapclient;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.types.Message;
|
||||||
|
|
||||||
|
interface MessagingBridge {
|
||||||
|
@nullable Message fetchMessage(String conversationId, String clientMessageId);
|
||||||
|
|
||||||
|
@nullable Message fetchMessageByServerId(String conversationId, String serverMessageId);
|
||||||
|
|
||||||
|
@nullable List<Message> fetchConversationWithMessagesPaginated(String conversationId, int limit, long beforeMessageId);
|
||||||
|
|
||||||
|
@nullable String updateMessage(String conversationId, String clientMessageId, String messageUpdate);
|
||||||
|
|
||||||
|
@nullable String getOneToOneConversationId(String userId);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package me.rhunk.snapenhance.bridge.snapclient.types;
|
||||||
|
|
||||||
|
parcelable Message {
|
||||||
|
String conversationId;
|
||||||
|
String senderId;
|
||||||
|
int contentType;
|
||||||
|
long clientMessageId;
|
||||||
|
long serverMessageId;
|
||||||
|
byte[] content;
|
||||||
|
List<String> mediaReferences;
|
||||||
|
}
|
@ -19,6 +19,7 @@ import me.rhunk.snapenhance.core.bridge.loadFromBridge
|
|||||||
import me.rhunk.snapenhance.core.data.SnapClassCache
|
import me.rhunk.snapenhance.core.data.SnapClassCache
|
||||||
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
|
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
|
||||||
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
|
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
|
||||||
|
import me.rhunk.snapenhance.core.messaging.CoreMessagingBridge
|
||||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||||
import me.rhunk.snapenhance.core.util.hook.hook
|
import me.rhunk.snapenhance.core.util.hook.hook
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@ -116,6 +117,7 @@ class SnapEnhance {
|
|||||||
logCritical(null, throwable)
|
logCritical(null, throwable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bridgeClient.registerMessagingBridge(CoreMessagingBridge(this))
|
||||||
|
|
||||||
reloadConfig()
|
reloadConfig()
|
||||||
actionManager.init()
|
actionManager.init()
|
||||||
|
@ -25,16 +25,7 @@ import java.io.File
|
|||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class ExportChatMessages : AbstractAction() {
|
class ExportChatMessages : AbstractAction() {
|
||||||
private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") }
|
|
||||||
|
|
||||||
private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") }
|
private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") }
|
||||||
|
|
||||||
private val enterConversationMethod by lazy {
|
|
||||||
context.classCache.conversationManager.methods.first { it.name == "enterConversation" }
|
|
||||||
}
|
|
||||||
private val exitConversationMethod by lazy {
|
|
||||||
context.classCache.conversationManager.methods.first { it.name == "exitConversation" }
|
|
||||||
}
|
|
||||||
private val fetchConversationWithMessagesPaginatedMethod by lazy {
|
private val fetchConversationWithMessagesPaginatedMethod by lazy {
|
||||||
context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" }
|
context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" }
|
||||||
}
|
}
|
||||||
@ -162,32 +153,6 @@ class ExportChatMessages : AbstractAction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun conversationAction(isEntering: Boolean, conversationId: String, conversationType: String?) = suspendCancellableCoroutine { continuation ->
|
|
||||||
val callback = CallbackBuilder(callbackClass)
|
|
||||||
.override("onSuccess") { _ ->
|
|
||||||
continuation.resumeWith(Result.success(Unit))
|
|
||||||
}
|
|
||||||
.override("onError") {
|
|
||||||
continuation.resumeWith(Result.failure(Exception("Failed to ${if (isEntering) "enter" else "exit"} conversation")))
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
if (isEntering) {
|
|
||||||
enterConversationMethod.invoke(
|
|
||||||
conversationManagerInstance,
|
|
||||||
SnapUUID.fromString(conversationId).instanceNonNull(),
|
|
||||||
enterConversationMethod.parameterTypes[1].enumConstants.first { it.toString() == conversationType },
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
exitConversationMethod.invoke(
|
|
||||||
conversationManagerInstance,
|
|
||||||
SnapUUID.fromString(conversationId).instanceNonNull(),
|
|
||||||
Long.MAX_VALUE,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int) = suspendCancellableCoroutine { continuation ->
|
private suspend fun fetchMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int) = suspendCancellableCoroutine { continuation ->
|
||||||
val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass)
|
val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass)
|
||||||
.override("onFetchConversationWithMessagesComplete") { param ->
|
.override("onFetchConversationWithMessagesComplete") { param ->
|
||||||
@ -213,10 +178,6 @@ class ExportChatMessages : AbstractAction() {
|
|||||||
val conversationId = friendFeedEntry.key!!
|
val conversationId = friendFeedEntry.key!!
|
||||||
val conversationName = friendFeedEntry.feedDisplayName ?: friendFeedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
|
val conversationName = friendFeedEntry.feedDisplayName ?: friendFeedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
|
||||||
|
|
||||||
runCatching {
|
|
||||||
conversationAction(true, conversationId, if (friendFeedEntry.feedDisplayName != null) "USERCREATEDGROUP" else "ONEONONE")
|
|
||||||
}
|
|
||||||
|
|
||||||
logDialog(context.translation.format("chat_export.exporting_message", "conversation" to conversationName))
|
logDialog(context.translation.format("chat_export.exporting_message", "conversation" to conversationName))
|
||||||
|
|
||||||
val foundMessages = fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).toMutableList()
|
val foundMessages = fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).toMutableList()
|
||||||
@ -266,10 +227,6 @@ class ExportChatMessages : AbstractAction() {
|
|||||||
logDialog("\n" + context.translation.format("chat_export.exported_to",
|
logDialog("\n" + context.translation.format("chat_export.exported_to",
|
||||||
"path" to outputFile.absolutePath.toString()
|
"path" to outputFile.absolutePath.toString()
|
||||||
) + "\n")
|
) + "\n")
|
||||||
|
|
||||||
runCatching {
|
|
||||||
conversationAction(false, conversationId, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportChatForConversations(conversations: List<FriendFeedEntry>) {
|
private fun exportChatForConversations(conversations: List<FriendFeedEntry>) {
|
||||||
|
@ -16,6 +16,7 @@ import me.rhunk.snapenhance.bridge.DownloadCallback
|
|||||||
import me.rhunk.snapenhance.bridge.SyncCallback
|
import me.rhunk.snapenhance.bridge.SyncCallback
|
||||||
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface
|
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface
|
||||||
import me.rhunk.snapenhance.bridge.scripting.IScripting
|
import me.rhunk.snapenhance.bridge.scripting.IScripting
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
||||||
import me.rhunk.snapenhance.common.BuildConfig
|
import me.rhunk.snapenhance.common.BuildConfig
|
||||||
import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper
|
import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper
|
||||||
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
|
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
|
||||||
@ -147,6 +148,8 @@ class BridgeClient(
|
|||||||
|
|
||||||
fun getMessageLogger() = service.messageLogger
|
fun getMessageLogger() = service.messageLogger
|
||||||
|
|
||||||
|
fun registerMessagingBridge(bridge: MessagingBridge) = service.registerMessagingBridge(bridge)
|
||||||
|
|
||||||
fun openSettingsOverlay() = service.openSettingsOverlay()
|
fun openSettingsOverlay() = service.openSettingsOverlay()
|
||||||
fun closeSettingsOverlay() = service.closeSettingsOverlay()
|
fun closeSettingsOverlay() = service.closeSettingsOverlay()
|
||||||
|
|
||||||
|
@ -60,6 +60,10 @@ object MessageDecoder {
|
|||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEncodedMediaReferences(messageContent: MessageContent): List<String> {
|
||||||
|
return getEncodedMediaReferences(gson.toJsonTree(messageContent.instanceNonNull()))
|
||||||
|
}
|
||||||
|
|
||||||
fun getMediaReferences(messageContent: JsonElement): List<JsonElement> {
|
fun getMediaReferences(messageContent: JsonElement): List<JsonElement> {
|
||||||
return messageContent.asJsonObject.getAsJsonArray("mRemoteMediaReferences")
|
return messageContent.asJsonObject.getAsJsonArray("mRemoteMediaReferences")
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
package me.rhunk.snapenhance.core.messaging
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
||||||
|
import me.rhunk.snapenhance.bridge.snapclient.types.Message
|
||||||
|
import me.rhunk.snapenhance.core.ModContext
|
||||||
|
import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder
|
||||||
|
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||||
|
import me.rhunk.snapenhance.core.util.CallbackBuilder
|
||||||
|
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||||
|
|
||||||
|
|
||||||
|
fun me.rhunk.snapenhance.core.wrapper.impl.Message.toBridge(): Message {
|
||||||
|
return Message().also { output ->
|
||||||
|
output.conversationId = this.messageDescriptor.conversationId.toString()
|
||||||
|
output.senderId = this.senderId.toString()
|
||||||
|
output.clientMessageId = this.messageDescriptor.messageId
|
||||||
|
output.serverMessageId = this.orderKey
|
||||||
|
output.contentType = this.messageContent.contentType?.id ?: -1
|
||||||
|
output.content = this.messageContent.content
|
||||||
|
output.mediaReferences = MessageDecoder.getEncodedMediaReferences(this.messageContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CoreMessagingBridge(
|
||||||
|
private val context: ModContext
|
||||||
|
) : MessagingBridge.Stub() {
|
||||||
|
private val conversationManager get() = context.feature(Messaging::class).conversationManager
|
||||||
|
|
||||||
|
override fun fetchMessage(conversationId: String, clientMessageId: String): Message? {
|
||||||
|
return runBlocking {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val callback = CallbackBuilder(
|
||||||
|
context.mappings.getMappedClass("callbacks", "FetchMessageCallback")
|
||||||
|
).override("onFetchMessageComplete") { param ->
|
||||||
|
val message = me.rhunk.snapenhance.core.wrapper.impl.Message(param.arg(1)).toBridge()
|
||||||
|
continuation.resumeWith(Result.success(message))
|
||||||
|
}
|
||||||
|
.override("onServerRequest", shouldUnhook = false) {}
|
||||||
|
.override("onError") {
|
||||||
|
continuation.resumeWith(Result.success(null))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
context.classCache.conversationManager.methods.first { it.name == "fetchMessage" }.invoke(
|
||||||
|
conversationManager,
|
||||||
|
SnapUUID.fromString(conversationId).instanceNonNull(),
|
||||||
|
clientMessageId,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchMessageByServerId(
|
||||||
|
conversationId: String,
|
||||||
|
serverMessageId: String
|
||||||
|
): Message? {
|
||||||
|
return runBlocking {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val callback = CallbackBuilder(
|
||||||
|
context.mappings.getMappedClass("callbacks", "FetchMessageCallback")
|
||||||
|
).override("onFetchMessageComplete") { param ->
|
||||||
|
val message = me.rhunk.snapenhance.core.wrapper.impl.Message(param.arg(1)).toBridge()
|
||||||
|
continuation.resumeWith(Result.success(message))
|
||||||
|
}
|
||||||
|
.override("onServerRequest", shouldUnhook = false) {}
|
||||||
|
.override("onError") {
|
||||||
|
continuation.resumeWith(Result.success(null))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val serverMessageIdentifier = context.androidContext.classLoader.loadClass("com.snapchat.client.messaging.ServerMessageIdentifier")
|
||||||
|
.getConstructor(context.classCache.snapUUID, Long::class.javaPrimitiveType)
|
||||||
|
.newInstance(SnapUUID.fromString(conversationId).instanceNonNull(), serverMessageId.toLong())
|
||||||
|
|
||||||
|
context.classCache.conversationManager.methods.first { it.name == "fetchMessageByServerId" }.invoke(
|
||||||
|
conversationManager,
|
||||||
|
serverMessageIdentifier,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchConversationWithMessagesPaginated(
|
||||||
|
conversationId: String,
|
||||||
|
limit: Int,
|
||||||
|
beforeMessageId: Long
|
||||||
|
): List<Message>? {
|
||||||
|
return runBlocking {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val callback = CallbackBuilder(
|
||||||
|
context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")
|
||||||
|
).override("onFetchConversationWithMessagesComplete") { param ->
|
||||||
|
val messagesList = param.arg<List<*>>(1).map {
|
||||||
|
me.rhunk.snapenhance.core.wrapper.impl.Message(it).toBridge()
|
||||||
|
}
|
||||||
|
continuation.resumeWith(Result.success(messagesList))
|
||||||
|
}
|
||||||
|
.override("onServerRequest", shouldUnhook = false) {}
|
||||||
|
.override("onError") {
|
||||||
|
continuation.resumeWith(Result.success(null))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" }.invoke(
|
||||||
|
conversationManager,
|
||||||
|
SnapUUID.fromString(conversationId).instanceNonNull(),
|
||||||
|
beforeMessageId,
|
||||||
|
limit,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateMessage(
|
||||||
|
conversationId: String,
|
||||||
|
clientMessageId: String,
|
||||||
|
messageUpdate: String
|
||||||
|
): String? {
|
||||||
|
return runBlocking {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val callback = CallbackBuilder(
|
||||||
|
context.mappings.getMappedClass("callbacks", "Callback")
|
||||||
|
).override("onSuccess") {
|
||||||
|
continuation.resumeWith(Result.success(null))
|
||||||
|
}
|
||||||
|
.override("onError") {
|
||||||
|
continuation.resumeWith(Result.success(it.arg<Any>(0).toString()))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
context.classCache.conversationManager.methods.first { it.name == "updateMessage" }.invoke(
|
||||||
|
conversationManager,
|
||||||
|
SnapUUID.fromString(conversationId).instanceNonNull(),
|
||||||
|
clientMessageId,
|
||||||
|
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == messageUpdate },
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOneToOneConversationId(userId: String) = context.database.getConversationLinkFromUserId(userId)?.clientConversationId
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user