Skip to content

Commit

Permalink
Merge pull request #149 from rodit/new-chat-menu
Browse files Browse the repository at this point in the history
Add support for new chat context menu
  • Loading branch information
rodit authored May 20, 2022
2 parents e375852 + f27f4ec commit 8a553ab
Show file tree
Hide file tree
Showing 17 changed files with 463 additions and 161 deletions.
Binary file modified app/libs/snapmod.jar
Binary file not shown.
22 changes: 22 additions & 0 deletions app/src/main/java/xyz/rodit/snapmod/DelegateProxy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package xyz.rodit.snapmod

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

typealias DelegateFunction = (Any, Array<Any>) -> Any?

class DelegateProxy(private val delegate: DelegateFunction) : InvocationHandler {

override fun invoke(target: Any, method: Method, args: Array<Any>?): Any? {
return delegate(target, args ?: emptyArray())
}
}

fun Class<*>.createDelegate(classLoader: ClassLoader, delegate: DelegateFunction): Any {
return Proxy.newProxyInstance(
classLoader,
arrayOf(this),
DelegateProxy(delegate)
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package xyz.rodit.snapmod.features

import xyz.rodit.snapmod.features.chatmenu.ChatMenuModifier
import xyz.rodit.snapmod.features.chatmenu.new.NewChatMenuModifier
import xyz.rodit.snapmod.features.conversations.*
import xyz.rodit.snapmod.features.friendsfeed.FeedModifier
import xyz.rodit.snapmod.features.info.AdditionalFriendInfo
Expand All @@ -22,6 +23,7 @@ class FeatureManager(context: FeatureContext) : Contextual(context) {
fun load() {
// Chat context menu
add(::ChatMenuModifier)
add(::NewChatMenuModifier)

// Friends feed
add(::FeedModifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package xyz.rodit.snapmod.features.callbacks
import de.robv.android.xposed.XC_MethodHook
import xyz.rodit.dexsearch.client.xposed.MethodRef
import xyz.rodit.snapmod.isDummyProxy
import xyz.rodit.snapmod.mappings.DefaultFetchConversationCallback
import xyz.rodit.snapmod.mappings.DefaultFetchMessageCallback
import xyz.rodit.snapmod.util.before
import kotlin.reflect.KClass

Expand All @@ -28,4 +30,16 @@ class CallbackManager {
fun on(type: KClass<*>, method: MethodRef, callback: HookedCallback) {
callbacks.computeIfAbsent("${type.simpleName}:${method.name}") { mutableListOf() }.add(callback)
}

init {
hook(
DefaultFetchConversationCallback::class,
DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete
) { DefaultFetchConversationCallback.wrap(it).dummy }

hook(
DefaultFetchMessageCallback::class,
DefaultFetchMessageCallback.onFetchMessageComplete
) { DefaultFetchMessageCallback.wrap(it).dummy }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const val PIN_STRING_NAME = "action_menu_pin_conversation"

class ChatMenuModifier(context: FeatureContext) : Feature(context) {

private val plugins: MutableMap<String, MenuPlugin> = HashMap()
private val plugins = mutableMapOf<String, MenuPlugin>()

override fun init() {
registerPlugin(PreviewOption(context))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package xyz.rodit.snapmod.features.chatmenu

import android.content.Intent
import androidx.core.content.FileProvider
import xyz.rodit.snapmod.CustomResources.string.menu_option_export
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.mappings.SelectFriendsByUserIds
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import xyz.rodit.snapmod.features.chatmenu.shared.export

class ExportOption(context: FeatureContext):
ButtonOption(context, "export_chat", menu_option_export) {
Expand All @@ -17,41 +12,6 @@ class ExportOption(context: FeatureContext):
override fun handleEvent(data: String?) {
if (data == null) return

val (messages, senders) = context.arroyo.getAllMessages(data)
val friendData =
context.instances.friendsRepository.selectFriendsByUserIds(senders.toList())
val senderMap = friendData.map(SelectFriendsByUserIds::wrap).associateBy { u -> u.userId }

val dateFormat = SimpleDateFormat("dd/MM/yyyy, HH:mm:ss", Locale.getDefault())

val temp = File.createTempFile(
"Snapchat Export ",
".txt",
File(context.appContext.filesDir, "file_manager/media")
)
temp.deleteOnExit()
temp.bufferedWriter().use {
messages.forEach { m ->
val username = senderMap[m.senderId]?.displayName ?: "Unknown"
val dateTime = dateFormat.format(m.timestamp)
it.append(dateTime)
.append(" - ")
.append(username)
.append(": ")
.appendLine(m.content)
}
}

val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(
Intent.EXTRA_STREAM,
FileProvider.getUriForFile(
context.appContext,
"com.snapchat.android.media.fileprovider",
temp
)
)
context.activity?.startActivity(Intent.createChooser(intent, "Export Chat"))
export(context, data)
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
package xyz.rodit.snapmod.features.chatmenu

import android.app.AlertDialog
import de.robv.android.xposed.XC_MethodHook
import xyz.rodit.snapmod.CustomResources.string.menu_option_preview
import xyz.rodit.snapmod.createDummyProxy
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.mappings.*
import xyz.rodit.snapmod.util.toSnapUUID
import xyz.rodit.snapmod.util.toUUIDString
import java.lang.Integer.min
import xyz.rodit.snapmod.features.chatmenu.shared.previewChat

class PreviewOption(context: FeatureContext) :
ButtonOption(context, "preview", menu_option_preview) {
Expand All @@ -18,79 +12,6 @@ class PreviewOption(context: FeatureContext) :
override fun handleEvent(data: String?) {
if (data == null) return

val uuid = data.toSnapUUID()
val proxy =
ConversationDummyInterface.wrap(
ConversationDummyInterface.getMappedClass().createDummyProxy(context.classLoader)
)

context.callbacks.on(
DefaultFetchConversationCallback::class,
DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete,
this::displayPreview
)

context.instances.conversationManager.fetchConversationWithMessages(
uuid,
DefaultFetchConversationCallback(proxy, uuid, false)
)
}

override fun performHooks() {
context.callbacks.hook(
DefaultFetchConversationCallback::class,
DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete
) { DefaultFetchConversationCallback.wrap(it).dummy }
}

private fun displayPreview(param: XC_MethodHook.MethodHookParam): Boolean {
val conversation = Conversation.wrap(param.args[0])

val userIds = conversation.participants.map(Participant::wrap)
.map { p -> (p.participantId.id as ByteArray).toUUIDString() }
val friendData = context.instances.friendsRepository.selectFriendsByUserIds(userIds)
val userMap = friendData.map(SelectFriendsByUserIds::wrap).associateBy { u -> u.userId }

val messageList = param.args[1] as List<*>
val previewText = StringBuilder()
if (messageList.isEmpty()) previewText.append("No messages available.")
else {
val numMessages =
min(context.config.getInt("preview_messages_count", 5), messageList.size)
previewText.append("Last ").append(numMessages).append(" messages:")
messageList.takeLast(numMessages)
.map(Message::wrap).forEach { m ->
run {
val uuidString = m.senderId.toUUIDString()
val displayName = userMap[uuidString]?.displayName ?: "Unknown"
previewText.append('\n').append(displayName).append(": ")
if (m.messageContent.contentType.instance == ContentType.CHAT().instance) {
val chatMessage =
NanoMessageContent.parse(m.messageContent.content).chatMessageContent.content
previewText.append(chatMessage)
} else {
previewText.append(m.messageContent.contentType.instance)
}
}
}
}

userMap.values.find { f -> f.streakExpiration ?: 0L > 0L }?.let { f ->
val hourDiff =
(f.streakExpiration - System.currentTimeMillis()).toDouble() / 3600000.0
previewText.append("\n\nStreak Expires in ")
.append(String.format("%.1f", hourDiff))
.append(" hours")
}

context.activity?.runOnUiThread {
AlertDialog.Builder(context.activity)
.setTitle(if (conversation.title.isNullOrBlank()) "Chat Preview" else conversation.title)
.setMessage(previewText)
.setPositiveButton("Ok") { _, _ -> }
.show()
}

return true
previewChat(context, data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package xyz.rodit.snapmod.features.chatmenu.new

abstract class MenuPlugin {

abstract fun shouldCreate(): Boolean

abstract fun createModel(key: String): Any

open fun performHooks() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package xyz.rodit.snapmod.features.chatmenu.new

import xyz.rodit.snapmod.createDelegate
import xyz.rodit.snapmod.features.Feature
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.features.chatmenu.shared.export
import xyz.rodit.snapmod.features.chatmenu.shared.previewChat
import xyz.rodit.snapmod.mappings.*
import xyz.rodit.snapmod.util.before

class NewChatMenuModifier(context: FeatureContext) : Feature(context) {

private val plugins = mutableListOf<MenuPlugin>()

override fun init() {
registerPlain("Export") { export(context, it) }
registerPlain("Preview") { previewChat(context, it) }

registerSwitch("pinning", "Pin Conversation") { it.pinned }
registerSwitch("stealth", "Stealth Mode") { it.stealth }
registerSwitch("auto_save", "Auto-Save Messages") { it.autoSave }
registerSwitch("auto_download", "Auto-Download Snaps") { it.autoDownload }
}

private fun registerPlugin(plugin: MenuPlugin) {
plugins.add(plugin)
}

private fun registerPlain(text: String, click: ClickHandler) {
registerPlugin(PlainOption(context, text, click))
}

private fun registerSwitch(name: String, text: String, manager: Manager) {
registerPlugin(SwitchOption(context, name, text, manager))
}

override fun performHooks() {
// Force new chat action menu
ProfileActionSheetChooser.choose.before {
it.args[0] = context.config.getBoolean("enable_new_chat_menu", true)
}

// Add subsection
ProfileActionSheetCreator.apply.before {
if (it.args[0] !is List<*>) return@before

val newItems = (it.args[0] as List<*>).toMutableList()
val creator = ProfileActionSheetCreator.wrap(it.thisObject)
val nestedContext = NestedActionMenuContext.wrap(creator.nestedContext)
val actionContext = ActionMenuContext.wrap(creator.actionMenuContext)
val key = actionContext.feedInfo.key

val subOptions = plugins.filter(MenuPlugin::shouldCreate).map { p ->
p.createModel(key)
}
val clickProxy =
Func0.getMappedClass().createDelegate(context.classLoader) { _, _ ->
NestedActionMenuContext.display(
nestedContext,
"SnapMod",
subOptions
)
null
}
val snapModSettings =
ActionClickableCaret("SnapMod Settings", null, Func0.wrap(clickProxy)).instance
newItems.add(snapModSettings)

it.args[0] = newItems
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package xyz.rodit.snapmod.features.chatmenu.new

import xyz.rodit.snapmod.createDelegate
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.mappings.ActionPlain
import xyz.rodit.snapmod.mappings.Func0

typealias ClickHandler = (key: String) -> Unit

class PlainOption(
private val context: FeatureContext,
private val text: String,
private val click: ClickHandler
) : MenuPlugin() {

override fun shouldCreate() = true

override fun createModel(key: String): Any = ActionPlain(
text,
Func0.wrap(Func0.getMappedClass().createDelegate(context.classLoader) { _, _ ->
click(key)
})
).instance
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package xyz.rodit.snapmod.features.chatmenu.new

import xyz.rodit.snapmod.createDelegate
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.mappings.ActionSwitch
import xyz.rodit.snapmod.mappings.Func1
import xyz.rodit.snapmod.util.ConversationManager
import xyz.rodit.snapmod.util.getList

typealias Manager = (FeatureContext) -> ConversationManager

class SwitchOption(
private val context: FeatureContext,
private val name: String,
private val text: String,
private val manager: Manager
) : MenuPlugin() {

override fun shouldCreate() = !context.config.getList("hidden_chat_options").contains(name)

override fun createModel(key: String): Any = ActionSwitch(
text,
manager(context).isEnabled(key),
Func1.wrap(Func1.getMappedClass().createDelegate(context.classLoader) { _, _ -> true }),
Func1.wrap(Func1.getMappedClass().createDelegate(context.classLoader) { _, _ ->
manager(context).toggle(key)
true
}),
null,
0
).instance
}
Loading

0 comments on commit 8a553ab

Please sign in to comment.