move emoji reaction code to repo and impl

This commit is contained in:
Ben Iofel
2025-09-02 18:45:52 -04:00
parent b4bc87a479
commit c1dcf8805d
5 changed files with 97 additions and 69 deletions
@@ -16,65 +16,25 @@
* You should have received a copy of the GNU General Public License
* along with QUIK. If not, see <http://www.gnu.org/licenses/>.
*/
package dev.octoshrimpy.quik.util
package dev.octoshrimpy.quik.repository
import dev.octoshrimpy.quik.manager.KeyManager
import dev.octoshrimpy.quik.model.EmojiReaction
import dev.octoshrimpy.quik.model.Message
import dev.octoshrimpy.quik.repository.EmojiReactionUtils.reactionPatterns
import dev.octoshrimpy.quik.repository.EmojiReactionUtils.removalPatterns
import io.realm.Realm
import io.realm.Sort
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.iterator
data class ParsedEmojiReaction(val emoji: String, val originalMessage: String, val isRemoval: Boolean = false)
object EmojiReactionUtils {
private fun tapback(emoji: String, isRemoval: Boolean = false): (MatchResult) -> ParsedEmojiReaction {
return { match ->
ParsedEmojiReaction(emoji, match.groupValues[1], isRemoval)
}
}
val reactionPatterns: Map<Regex, (MatchResult) -> ParsedEmojiReaction?> = mapOf(
// Google Messages - https://github.com/octoshrimpy/quik/issues/152#issuecomment-2330183516
Regex("^\u200A\u200B(.+?)\u200B to \u201C\u200A(.+?)\u200A\u201D\u200A$") to { match ->
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2])
},
// iOS
Regex("^Reacted (.+?) to \u201C(.+?)\u201D$") to { match ->
if (match.groupValues[1] == "with a sticker")
null
else
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2])
},
// iOS tapbacks
Regex("^Loved \u201C(.+?)\u201D$") to tapback("❤️"),
Regex("^Liked \u201C(.+?)\u201D$") to tapback("👍"),
Regex("^Disliked \u201C(.+?)\u201D$") to tapback("👎"),
Regex("^Laughed at \u201C(.+?)\u201D$") to tapback("😂"),
Regex("^Emphasized \u201C(.+?)\u201D$") to tapback("‼️"),
Regex("^Questioned \u201C(.+?)\u201D$") to tapback("❓"),
)
val removalPatterns: Map<Regex, (MatchResult) -> ParsedEmojiReaction?> = mapOf(
// Google Messages
Regex("^\u200ARemoved \u200C(.+?)\u200C from \u201C\u200A(.+?)\u200A\u201D\u200A$") to { match ->
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2], isRemoval = true)
},
// iOS tapbacks
Regex("^Removed a heart from \u201C(.+?)\u201D$") to tapback("❤️", true),
Regex("^Removed a like from \u201C(.+?)\u201D$") to tapback("👍", true),
Regex("^Removed a dislike from \u201C(.+?)\u201D$") to tapback("👎", true),
Regex("^Removed a laugh from \u201C(.+?)\u201D$") to tapback("😂", true),
Regex("^Removed an exclamation from \u201C(.+?)\u201D$") to tapback("‼️", true),
Regex("^Removed a question mark from \u201C(.+?)\u201D$") to tapback("❓", true),
// iOS emoji - keep this below tapbacks as this regex would otherwise also match the patterns above
Regex("^Removed (.+?) from \u201C(.+?)\u201D$") to { match ->
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2], isRemoval = true)
},
)
fun parseEmojiReaction(body: String): ParsedEmojiReaction? {
class EmojiReactionRepositoryImpl @Inject constructor(
private val keyManager: KeyManager
) : EmojiReactionRepository {
override fun parseEmojiReaction(body: String): ParsedEmojiReaction? {
val removal = parseRemoval(body)
if (removal != null) return removal
@@ -111,7 +71,7 @@ object EmojiReactionUtils {
* Search for messages in the same thread with matching text content
* We'll search recent messages first (within reasonable time window)
*/
fun findTargetMessage(threadId: Long, originalMessageText: String, realm: Realm): Message? {
override fun findTargetMessage(threadId: Long, originalMessageText: String, realm: Realm): Message? {
val messages = realm.where(Message::class.java)
.equalTo("threadId", threadId)
.sort("date", Sort.DESCENDING)
@@ -158,11 +118,10 @@ object EmojiReactionUtils {
realm.insertOrUpdate(reactionMessage)
}
fun saveEmojiReaction(
override fun saveEmojiReaction(
reactionMessage: Message,
parsedReaction: ParsedEmojiReaction,
targetMessage: Message?,
keyManager: KeyManager,
realm: Realm,
) {
if (parsedReaction.isRemoval) {
@@ -191,4 +150,52 @@ object EmojiReactionUtils {
Timber.w("Saved emoji reaction without target message: ${reaction.emoji}")
}
}
}
object EmojiReactionUtils {
private fun tapback(emoji: String, isRemoval: Boolean = false): (MatchResult) -> ParsedEmojiReaction {
return { match ->
ParsedEmojiReaction(emoji, match.groupValues[1], isRemoval)
}
}
val reactionPatterns: Map<Regex, (MatchResult) -> ParsedEmojiReaction?> = mapOf(
// Google Messages - https://github.com/octoshrimpy/quik/issues/152#issuecomment-2330183516
Regex("^\u200A\u200B(.+?)\u200B to \u201C\u200A(.+?)\u200A\u201D\u200A$") to { match ->
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2])
},
// iOS
Regex("^Reacted (.+?) to \u201C(.+?)\u201D$") to { match ->
if (match.groupValues[1] == "with a sticker")
null
else
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2])
},
// iOS tapbacks
Regex("^Loved \u201C(.+?)\u201D$") to tapback("❤️"),
Regex("^Liked \u201C(.+?)\u201D$") to tapback("👍"),
Regex("^Disliked \u201C(.+?)\u201D$") to tapback("👎"),
Regex("^Laughed at \u201C(.+?)\u201D$") to tapback("😂"),
Regex("^Emphasized \u201C(.+?)\u201D$") to tapback("‼️"),
Regex("^Questioned \u201C(.+?)\u201D$") to tapback("❓"),
)
val removalPatterns: Map<Regex, (MatchResult) -> ParsedEmojiReaction?> = mapOf(
// Google Messages
Regex("^\u200ARemoved \u200C(.+?)\u200C from \u201C\u200A(.+?)\u200A\u201D\u200A$") to { match ->
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2], isRemoval = true)
},
// iOS tapbacks
Regex("^Removed a heart from \u201C(.+?)\u201D$") to tapback("❤️", true),
Regex("^Removed a like from \u201C(.+?)\u201D$") to tapback("👍", true),
Regex("^Removed a dislike from \u201C(.+?)\u201D$") to tapback("👎", true),
Regex("^Removed a laugh from \u201C(.+?)\u201D$") to tapback("😂", true),
Regex("^Removed an exclamation from \u201C(.+?)\u201D$") to tapback("‼️", true),
Regex("^Removed a question mark from \u201C(.+?)\u201D$") to tapback("❓", true),
// iOS emoji - keep this below tapbacks as this regex would otherwise also match the patterns above
Regex("^Removed (.+?) from \u201C(.+?)\u201D$") to { match ->
ParsedEmojiReaction(match.groupValues[1], match.groupValues[2], isRemoval = true)
},
)
}
@@ -57,7 +57,6 @@ import dev.octoshrimpy.quik.receiver.SendSmsReceiver
import dev.octoshrimpy.quik.receiver.SmsDeliveredReceiver
import dev.octoshrimpy.quik.receiver.SmsSentReceiver
import dev.octoshrimpy.quik.util.ImageUtils
import dev.octoshrimpy.quik.util.EmojiReactionUtils
import dev.octoshrimpy.quik.util.PhoneNumberUtils
import dev.octoshrimpy.quik.util.Preferences
import dev.octoshrimpy.quik.util.tryOrNull
@@ -79,6 +78,7 @@ open class MessageRepositoryImpl @Inject constructor(
private val phoneNumberUtils: PhoneNumberUtils,
private val prefs: Preferences,
private val syncRepository: SyncRepository,
private val reactions: EmojiReactionRepository,
) : MessageRepository {
companion object {
@@ -765,19 +765,18 @@ open class MessageRepositoryImpl @Inject constructor(
}
managedMessage?.let { savedMessage ->
val parsedReaction = EmojiReactionUtils.parseEmojiReaction(body)
val parsedReaction = reactions.parseEmojiReaction(body)
if (parsedReaction != null) {
val targetMessage = EmojiReactionUtils.findTargetMessage(
val targetMessage = reactions.findTargetMessage(
savedMessage.threadId,
parsedReaction.originalMessage,
realm
)
realm.executeTransaction {
EmojiReactionUtils.saveEmojiReaction(
reactions.saveEmojiReaction(
savedMessage,
parsedReaction,
targetMessage,
messageIds,
realm,
)
}
@@ -45,7 +45,6 @@ import dev.octoshrimpy.quik.model.MmsPart
import dev.octoshrimpy.quik.model.PhoneNumber
import dev.octoshrimpy.quik.model.Recipient
import dev.octoshrimpy.quik.model.SyncLog
import dev.octoshrimpy.quik.util.EmojiReactionUtils
import dev.octoshrimpy.quik.util.PhoneNumberUtils
import dev.octoshrimpy.quik.util.tryOrNull
import io.reactivex.subjects.BehaviorSubject
@@ -70,7 +69,8 @@ class SyncRepositoryImpl @Inject constructor(
private val cursorToContactGroupMember: CursorToContactGroupMember,
private val keys: KeyManager,
private val phoneNumberUtils: PhoneNumberUtils,
private val rxPrefs: RxSharedPreferences
private val rxPrefs: RxSharedPreferences,
private val reactions: EmojiReactionRepository,
) : SyncRepository {
override val syncProgress: Subject<SyncRepository.SyncProgress> =
@@ -248,18 +248,17 @@ class SyncRepositoryImpl @Inject constructor(
allMessages.forEach { message ->
val text = message.getText(false)
val parsedReaction = EmojiReactionUtils.parseEmojiReaction(text)
val parsedReaction = reactions.parseEmojiReaction(text)
if (parsedReaction != null) {
val targetMessage = EmojiReactionUtils.findTargetMessage(
val targetMessage = reactions.findTargetMessage(
message.threadId,
parsedReaction.originalMessage,
realm
)
EmojiReactionUtils.saveEmojiReaction(
reactions.saveEmojiReaction(
message,
parsedReaction,
targetMessage,
keys,
realm,
)
}
@@ -326,20 +325,19 @@ class SyncRepositoryImpl @Inject constructor(
insertOrUpdate()
val text = getText(false)
val parsedReaction = EmojiReactionUtils.parseEmojiReaction(text)
val parsedReaction = reactions.parseEmojiReaction(text)
if (parsedReaction != null) {
Realm.getDefaultInstance().use { realm ->
val targetMessage = EmojiReactionUtils.findTargetMessage(
val targetMessage = reactions.findTargetMessage(
threadId,
parsedReaction.originalMessage,
realm
)
realm.executeTransaction {
EmojiReactionUtils.saveEmojiReaction(
reactions.saveEmojiReaction(
this,
parsedReaction,
targetMessage,
keys,
realm,
)
}
@@ -0,0 +1,19 @@
package dev.octoshrimpy.quik.repository
import dev.octoshrimpy.quik.model.Message
import io.realm.Realm
data class ParsedEmojiReaction(val emoji: String, val originalMessage: String, val isRemoval: Boolean = false)
interface EmojiReactionRepository {
fun parseEmojiReaction(body: String): ParsedEmojiReaction?
fun findTargetMessage(threadId: Long, originalMessageText: String, realm: Realm): Message?
fun saveEmojiReaction(
reactionMessage: Message,
parsedReaction: ParsedEmojiReaction,
targetMessage: Message?,
realm: Realm,
)
}
@@ -81,6 +81,8 @@ import dev.octoshrimpy.quik.repository.ContactRepository
import dev.octoshrimpy.quik.repository.ContactRepositoryImpl
import dev.octoshrimpy.quik.repository.ConversationRepository
import dev.octoshrimpy.quik.repository.ConversationRepositoryImpl
import dev.octoshrimpy.quik.repository.EmojiReactionRepository
import dev.octoshrimpy.quik.repository.EmojiReactionRepositoryImpl
import dev.octoshrimpy.quik.repository.MessageContentFilterRepository
import dev.octoshrimpy.quik.repository.MessageContentFilterRepositoryImpl
import dev.octoshrimpy.quik.repository.MessageRepository
@@ -219,6 +221,9 @@ class AppModule(private var application: Application) {
@Provides
fun provideSyncRepository(repository: SyncRepositoryImpl): SyncRepository = repository
@Provides
fun provideEmojiReactionRepository(repository: EmojiReactionRepositoryImpl): EmojiReactionRepository = repository
// worker factory
@Provides
fun provideWorkerFactory(workerFactory: InjectionWorkerFactory): WorkerFactory = workerFactory