diff --git a/plugins/Myosotis/build.gradle.kts b/plugins/Myosotis/build.gradle.kts new file mode 100644 index 0000000..7bd110c --- /dev/null +++ b/plugins/Myosotis/build.gradle.kts @@ -0,0 +1,12 @@ +version = "1.0.0" +description = "Backports DM previews" + +aliucord { + // Changelog of your plugin + changelog.set(""" + # 1.0.0 + * Initial release >w< + """.trimIndent()) + + deploy.set(true) +} diff --git a/plugins/Myosotis/src/main/kotlin/moe/lava/awoocord/myosotis/Myosotis.kt b/plugins/Myosotis/src/main/kotlin/moe/lava/awoocord/myosotis/Myosotis.kt new file mode 100644 index 0000000..60ed811 --- /dev/null +++ b/plugins/Myosotis/src/main/kotlin/moe/lava/awoocord/myosotis/Myosotis.kt @@ -0,0 +1,205 @@ +package moe.lava.awoocord.myosotis + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.aliucord.Http +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.api.GatewayAPI +import com.aliucord.entities.Plugin +import com.aliucord.patcher.after +import com.aliucord.patcher.before +import com.aliucord.patcher.component1 +import com.aliucord.patcher.component2 +import com.aliucord.patcher.component3 +import com.aliucord.utils.ChannelUtils +import com.aliucord.utils.GsonUtils +import com.aliucord.utils.SerializedName +import com.aliucord.utils.accessField +import com.aliucord.wrappers.ChannelWrapper.Companion.id +import com.aliucord.wrappers.users.globalName +import com.discord.api.message.Message +import com.discord.databinding.WidgetChannelsListItemChannelPrivateBinding +import com.discord.models.domain.ModelMessageDelete +import com.discord.stores.StoreStream +import com.discord.utilities.color.ColorCompat +import com.discord.utilities.textprocessing.DiscordParser +import com.discord.utilities.textprocessing.MessagePreprocessor +import com.discord.utilities.textprocessing.MessageRenderContext +import com.discord.utilities.view.text.SimpleDraweeSpanTextView +import com.discord.widgets.channels.list.WidgetChannelsListAdapter +import com.discord.widgets.channels.list.items.ChannelListItem +import com.discord.widgets.channels.list.items.ChannelListItemPrivate +import com.discord.widgets.chat.list.adapter.`WidgetChatListAdapterItemMessage$getMessageRenderContext$1` +import com.discord.widgets.chat.list.adapter.`WidgetChatListAdapterItemMessage$getMessageRenderContext$4` +import com.google.gson.reflect.TypeToken +import com.lytefast.flexinput.R +import java.lang.ref.WeakReference + +private val WidgetChannelsListAdapter.ItemChannelPrivate.binding + by accessField() + +private val responseType = TypeToken.getParameterized(List::class.java, Message::class.java).type + +data class ChannelIdsPayload( + @SerializedName("channel_ids") val channelIds: List, +) + +data class MessageItem( + val id: Long, + val content: String?, +) + +fun Message.wrap(): MessageItem { + val author = this.e() + val authorName = if (author.id == StoreStream.getUsers().me.id) { + "You" + } else { + author.globalName ?: author.username + } + val content = this.i() + .takeIf { it.isNotEmpty() } + ?.let { content -> "$authorName: ${content.takeWhile { it != '\n' }}" } + + return MessageItem( + id = this.o(), + content = content, + ) +} + +fun SimpleDraweeSpanTextView.renderText(content: String, other: Pair) { + val me = StoreStream.getUsers().me + val meId = me.id + val meName = me.globalName ?: me.username + val processor = MessagePreprocessor(meId, listOf(), null, false, 50) + val parseChannelMessage = DiscordParser.parseChannelMessage( + context, + content, + MessageRenderContext( + context, + meId, + false, + mapOf(meId to meName, other), + StoreStream.getChannels().channelNames, + mapOf(), + R.b.colorTextLink, + `WidgetChatListAdapterItemMessage$getMessageRenderContext$1`.INSTANCE, + { }, + ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg), + ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg_visible), + { }, + { }, + `WidgetChatListAdapterItemMessage$getMessageRenderContext$4`(context) + ), + processor, + DiscordParser.ParserOptions.DEFAULT, + false + ) + setDraweeSpanStringBuilder(parseChannelMessage); +} + +@AliucordPlugin +class Myosotis : Plugin() { + var cache = mutableMapOf() + var adapterRef: WeakReference? = null + + override fun stop(context: Context) { patcher.unpatchAll() } + + override fun start(context: Context) { + GatewayAPI.onEvent("READY") { refreshAll() } + GatewayAPI.onEvent("RESUMED") { refreshAll() } + + patcher.after( + "onConfigure", + Int::class.java, + ChannelListItem::class.java, + ) { (_, _: Int, item: ChannelListItemPrivate) -> + cache[item.channel.id]?.let { msg -> + val content = msg.content + ?: return@let + + val descView = binding.d + descView.visibility = View.VISIBLE + val user = ChannelUtils.getDMRecipient(item.channel) + descView.renderText(content, user.id to (user.globalName ?: user.username)) + } + } + patcher.after( + RecyclerView::class.java, + FragmentManager::class.java, + ) { + adapterRef = WeakReference(this) + } + + patcher.before( + "handleMessageCreate", + Message::class.java + ) { (_, msg: Message) -> + handleMessageUpdate(msg) + } + + patcher.before( + "handleMessageUpdate", + Message::class.java + ) { (_, msg: Message) -> + handleMessageUpdate(msg) + } + + patcher.before( + "handleMessageDelete", + ModelMessageDelete::class.java + ) { (_, deleteModel: ModelMessageDelete) -> + cache[deleteModel.channelId]?.let { msg -> + if (msg.id in deleteModel.messageIds) { + cache.remove(deleteModel.channelId) + rerender(deleteModel.channelId) + } + } + } + } + + private fun handleMessageUpdate(msg: Message) { + val gid = msg.m() + if (gid == null) { + val channelId = msg.g() + + val oldMsgId = cache[channelId]?.id ?: 0 + if (msg.o() > oldMsgId) { + cache[channelId] = msg.wrap() + rerender(channelId) + } + } + } + + @OptIn(ExperimentalStdlibApi::class) + private fun refreshAll() { + val channels = StoreStream.getChannels().getChannelsForGuild(0) + .filterValues { it.D() == 1 } // type == Type.DM + .keys.take(100) + Utils.threadPool.execute { + val res = Http.Request.newDiscordRNRequest("/channels/preload-messages", "POST") + .executeWithJson(ChannelIdsPayload(channels)) + .json>(GsonUtils.gsonRestApi, responseType) + cache = mutableMapOf(*res.map { it.g() to it.wrap() }.toTypedArray()) + + Utils.mainThread.post { + @SuppressLint("NotifyDataSetChanged") // I DONT CARE HAHAHAAHJAHAAJHDLAHD + adapterRef?.get()?.notifyDataSetChanged() + } + } + } + + private fun rerender(id: Long) { + val adapter = adapterRef?.get() ?: return + val idx = adapter.internalData.indexOfFirst { it.key == "3$id" } + logger.info("found $idx for $id") + if (idx != -1) { + Utils.mainThread.post { + adapter.notifyItemChanged(idx) + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c8e9733..f6d7bdf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ val plugins = mapOf( "Clump" to "plugins/Bocchi", "Scout" to "plugins/Scout", "RoleBlocks" to "plugins/Zinnia", + "Glance" to "plugins/Myosotis", ) include(*plugins.keys.toTypedArray())