mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-05 09:52:13 +00:00
Add emoji reacts support (#889)
* feat: Add emoji reacts support * Remove message multi-selection * Add emoji reaction model * Add emoji reaction panel * Blur reacts panel background * Show emoji keyboard * Add emoji sprites * Update reaction proto * Emoji database updates * Emoji database refactor * Emoji reaction persistence * Optimize reactions retrieval * Fix emoji group query * Display emojis * Fix emoji persistence * Cleanup * Persistence refactor * Add reactions bottom sheet * Cleanup * Ui tweaks * React with any emoji * Show emoji react notifications * Remove reaction * Show reactions modal on long press * Click to react (+1) with an emoji * Click to react with an emoji * Enable emoji expand/collapse * fix: some compile issues from merge conflicts * fix: compile issues merging quote and media message UI * fix: xml IDs and adding in legacy is selected for future inclusion * Fix view constraints * Fix merge issue * Add message selection option in conversation context menu * Add sogs emoji integration * Handle sogs emoji reactions * Enable sending/deleting sogs emojis * fix: improve the visible message layout * fix: add file IDs to request parameters for message send (#940) * Fix open group polling from seqno instead of last hash (#939) * fix: reset seqno to get recent messages from open groups * build: upgrade build numbers * fix: actually run the migration * Using StringBuilder to construct request url * Fix reaction filter * fix: is_mms added in second projection query * Update default emojis * fix: include legacy and new open groups in server ID tracking (#941) * feat: add hidden moderator and admin roles, separated as they may be used independently in future (#942) * Cleanup * Fix view constraints * Add reactions capability check * Fix reactions alignment * Ui fixes * Display reactions list * feat: add formatted count strings * fix: account for negatives and add tests * Migrate old official open group locations for polling and adding (#932) * feat: adding in first part of open group migrations and tests for migration logic / helpers * feat: test code and migration logic for open groups in the case of no conflicts * feat: add in extra test cases and refactor code for migrator * refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it * refactor: joining open groups using OpenGroupUrlParser.kt now * fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2 * fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key * fix: room and server swapped parameters * fix: replace default server for config messages * fix: actually using public key to de-dupe didn't work for rooms * build: bump version code and name * Display reactions list on open groups for moderators * Ui tweaks * Ui tweaks for moderation * Refactor * fix: compile issue * fix: de-duping joined queries in the get X from cursor * Restore import * fix: colouring the reaction overlay scrubber * fix: highlight colour, show reaction count if 1 or above * Cleanup * fix: light mode accent * fix: light / dark mode themeing in reactions dialog fragment * Emoji notification blinded id check * fix: show reaction list correctly and pass isUserModerator to bind methods * fix: remove unnecessary places for the moderator * fix: X button for removing own react not showing up properly * feat: add clear all header view * fix: migrate the clear all to the correct location * fix: use display instead of base * Truncate emoji sender ids * feat: add notify thread function in thread db * Notify threads on reaction received * fix: design fixes for the reaction list * fix: emoji reactions bottom sheet dialog UI designs * feat: add unsupported emoji reaction * fix: crash and doing vector properly * Fix reaction database queries * Fix background open group adder job * Show new open group reactions * Fetch a maximum of 5 reactors * Handle open group reactions polling conflicts * Add count to user reaction * Show number of additional reactors * fix: unreads set same as the unread query * fix: design changes * fix: update dependency to improve flexboxlayout behaviour, design consistencies * Add select message icon and update long press menu items order and wording * Fix crash on reactors dialog * fix: colours and backgrounds to match designs * fix: add header in recipient item * fix: margins * fix: alignments and layout issues for emoji reactions view * feat: add overflow previews and logic for overflow * Dim action bar * Add emoji search * Search index fix * Set count for 1:1 and closed group reactions when inserting in local database * Use on screen toolbar to allow overlaying * Show/hide scroll to bottom button * feat: add extended properties so it doesn't collapse on re-bind * Cleanup * feat: prevent keeping extended on rebinding if we get a new message ID * fix: long press works on devices now, fix release lint issue and crash for emoji search DBs from emoji builds * Display message timestamp * Fix modal items alignment * fix: sort order and emoji count in compareTo * Scale down really large messages to fit * Prevent closed group crash * Fix reaction author Co-authored-by: charles <charles@oxen.io> Co-authored-by: jubb <hjubb@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.hamcrest.CoreMatchers.endsWith
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
@@ -14,6 +15,7 @@ import org.mockito.Mockito.verify
|
||||
import org.mockito.kotlin.any
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.BaseViewModelTest
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import org.thoughtcrime.securesms.repository.ResultOf
|
||||
@@ -22,12 +24,14 @@ import org.mockito.Mockito.`when` as whenever
|
||||
class ConversationViewModelTest: BaseViewModelTest() {
|
||||
|
||||
private val repository = mock(ConversationRepository::class.java)
|
||||
private val storage = mock(Storage::class.java)
|
||||
|
||||
private val threadId = 123L
|
||||
private val edKeyPair = mock(KeyPair::class.java)
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
private val viewModel: ConversationViewModel by lazy {
|
||||
ConversationViewModel(threadId, repository)
|
||||
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
||||
}
|
||||
|
||||
@Before
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
class NumberUtilTests {
|
||||
|
||||
@Test
|
||||
fun `it should display numbers less than 1000 as they are`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(900)
|
||||
assertEquals("900", formatString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should display exactly 1000 as 1k`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(1000)
|
||||
assertEquals("1k", formatString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should display numbers less than 10_000 properly`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(1300)
|
||||
assertEquals("1.3k", formatString)
|
||||
val multipleKFormatString = NumberUtil.getFormattedNumber(3100)
|
||||
assertEquals("3.1k", multipleKFormatString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should display zero properly`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(0)
|
||||
assertEquals("0", formatString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it shouldn't care about negative numbers`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(-10)
|
||||
assertEquals("-10", formatString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it shouldn't get about large negative numbers`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(-1200)
|
||||
assertEquals("-1.2k", formatString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should display numbers above 10k properly`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(12300)
|
||||
assertEquals("12.3k", formatString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should display numbers above 100k properly`() {
|
||||
val formatString = NumberUtil.getFormattedNumber(132560)
|
||||
assertEquals("132.5k", formatString)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.KStubbing
|
||||
import org.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.verifyNoMoreInteractions
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupMigrator
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupMigrator.OpenGroupMapping
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupMigrator.roomStub
|
||||
|
||||
class OpenGroupMigrationTests {
|
||||
|
||||
companion object {
|
||||
const val EXAMPLE_LEGACY_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e6f78656e"
|
||||
const val EXAMPLE_NEW_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e6f78656e"
|
||||
const val OXEN_STUB_HEX = "6f78656e"
|
||||
|
||||
const val EXAMPLE_LEGACY_SERVER_ID = "http://116.203.70.33.oxen"
|
||||
const val EXAMPLE_NEW_SERVER_ID = "https://open.getsession.org.oxen"
|
||||
|
||||
const val LEGACY_THREAD_ID = 1L
|
||||
const val NEW_THREAD_ID = 2L
|
||||
}
|
||||
|
||||
private fun legacyOpenGroupRecipient(additionalMocks: ((KStubbing<Recipient>) -> Unit) ? = null) = mock<Recipient> {
|
||||
on { address } doReturn Address.fromSerialized(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP)
|
||||
on { isOpenGroupRecipient } doReturn true
|
||||
additionalMocks?.let { it(this) }
|
||||
}
|
||||
|
||||
private fun newOpenGroupRecipient(additionalMocks: ((KStubbing<Recipient>) -> Unit) ? = null) = mock<Recipient> {
|
||||
on { address } doReturn Address.fromSerialized(EXAMPLE_NEW_ENCODED_OPEN_GROUP)
|
||||
on { isOpenGroupRecipient } doReturn true
|
||||
additionalMocks?.let { it(this) }
|
||||
}
|
||||
|
||||
private fun legacyThreadRecord(additionalRecipientMocks: ((KStubbing<Recipient>) -> Unit) ? = null, additionalThreadMocks: ((KStubbing<ThreadRecord>) -> Unit)? = null) = mock<ThreadRecord> {
|
||||
val returnedRecipient = legacyOpenGroupRecipient(additionalRecipientMocks)
|
||||
on { recipient } doReturn returnedRecipient
|
||||
on { threadId } doReturn LEGACY_THREAD_ID
|
||||
}
|
||||
|
||||
private fun newThreadRecord(additionalRecipientMocks: ((KStubbing<Recipient>) -> Unit)? = null, additionalThreadMocks: ((KStubbing<ThreadRecord>) -> Unit)? = null) = mock<ThreadRecord> {
|
||||
val returnedRecipient = newOpenGroupRecipient(additionalRecipientMocks)
|
||||
on { recipient } doReturn returnedRecipient
|
||||
on { threadId } doReturn NEW_THREAD_ID
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should generate the correct room stubs for legacy groups`() {
|
||||
val mockRecipient = legacyOpenGroupRecipient()
|
||||
assertEquals(OXEN_STUB_HEX, mockRecipient.roomStub())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should generate the correct room stubs for new groups`() {
|
||||
val mockNewRecipient = newOpenGroupRecipient()
|
||||
assertEquals(OXEN_STUB_HEX, mockNewRecipient.roomStub())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should return correct mappings`() {
|
||||
val legacyThread = legacyThreadRecord()
|
||||
val newThread = newThreadRecord()
|
||||
|
||||
val expectedMapping = listOf(
|
||||
OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, NEW_THREAD_ID)
|
||||
)
|
||||
|
||||
assertTrue(expectedMapping.containsAll(OpenGroupMigrator.getExistingMappings(listOf(legacyThread), listOf(newThread))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should return no mappings if there are no legacy open groups`() {
|
||||
val mappings = OpenGroupMigrator.getExistingMappings(listOf(), listOf())
|
||||
assertTrue(mappings.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should return no mappings if there are only new open groups`() {
|
||||
val newThread = newThreadRecord()
|
||||
val mappings = OpenGroupMigrator.getExistingMappings(emptyList(), listOf(newThread))
|
||||
assertTrue(mappings.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should return null new thread in mappings if there are only legacy open groups`() {
|
||||
val legacyThread = legacyThreadRecord()
|
||||
val mappings = OpenGroupMigrator.getExistingMappings(listOf(legacyThread), emptyList())
|
||||
val expectedMappings = listOf(
|
||||
OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, null)
|
||||
)
|
||||
assertTrue(expectedMappings.containsAll(mappings))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test migration thread DB calls legacy and returns if no legacy official groups`() {
|
||||
val mockedThreadDb = mock<ThreadDatabase> {
|
||||
on { legacyOxenOpenGroups } doReturn emptyList()
|
||||
}
|
||||
val mockedDbComponent = mock<DatabaseComponent> {
|
||||
on { threadDatabase() } doReturn mockedThreadDb
|
||||
}
|
||||
|
||||
OpenGroupMigrator.migrate(mockedDbComponent)
|
||||
|
||||
verify(mockedDbComponent).threadDatabase()
|
||||
verify(mockedThreadDb).legacyOxenOpenGroups
|
||||
verifyNoMoreInteractions(mockedThreadDb)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should migrate on thread, group and loki dbs with correct values for legacy only migration`() {
|
||||
// mock threadDB
|
||||
val capturedThreadId = argumentCaptor<Long>()
|
||||
val capturedNewEncoded = argumentCaptor<String>()
|
||||
val mockedThreadDb = mock<ThreadDatabase> {
|
||||
val legacyThreadRecord = legacyThreadRecord()
|
||||
on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord)
|
||||
on { httpsOxenOpenGroups } doReturn emptyList()
|
||||
on { migrateEncodedGroup(capturedThreadId.capture(), capturedNewEncoded.capture()) } doAnswer {}
|
||||
}
|
||||
|
||||
// mock groupDB
|
||||
val capturedGroupLegacyEncoded = argumentCaptor<String>()
|
||||
val capturedGroupNewEncoded = argumentCaptor<String>()
|
||||
val mockedGroupDb = mock<GroupDatabase> {
|
||||
on {
|
||||
migrateEncodedGroup(
|
||||
capturedGroupLegacyEncoded.capture(),
|
||||
capturedGroupNewEncoded.capture()
|
||||
)
|
||||
} doAnswer {}
|
||||
}
|
||||
|
||||
// mock LokiAPIDB
|
||||
val capturedLokiLegacyGroup = argumentCaptor<String>()
|
||||
val capturedLokiNewGroup = argumentCaptor<String>()
|
||||
val mockedLokiApi = mock<LokiAPIDatabase> {
|
||||
on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {}
|
||||
}
|
||||
|
||||
val pubKey = OpenGroupApi.defaultServerPublicKey
|
||||
val room = "oxen"
|
||||
val legacyServer = OpenGroupApi.legacyDefaultServer
|
||||
val newServer = OpenGroupApi.defaultServer
|
||||
|
||||
val lokiThreadOpenGroup = argumentCaptor<OpenGroup>()
|
||||
val mockedLokiThreadDb = mock<LokiThreadDatabase> {
|
||||
on { getOpenGroupChat(eq(LEGACY_THREAD_ID)) } doReturn OpenGroup(legacyServer, room, "Oxen", 0, pubKey)
|
||||
on { setOpenGroupChat(lokiThreadOpenGroup.capture(), eq(LEGACY_THREAD_ID)) } doAnswer {}
|
||||
}
|
||||
|
||||
val mockedDbComponent = mock<DatabaseComponent> {
|
||||
on { threadDatabase() } doReturn mockedThreadDb
|
||||
on { groupDatabase() } doReturn mockedGroupDb
|
||||
on { lokiAPIDatabase() } doReturn mockedLokiApi
|
||||
on { lokiThreadDatabase() } doReturn mockedLokiThreadDb
|
||||
}
|
||||
|
||||
OpenGroupMigrator.migrate(mockedDbComponent)
|
||||
|
||||
// expect threadDB migration to reflect new thread values:
|
||||
// thread ID = 1, encoded ID = new encoded ID
|
||||
assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue)
|
||||
assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedNewEncoded.firstValue)
|
||||
|
||||
// expect groupDB migration to reflect new thread values:
|
||||
// legacy encoded ID, new encoded ID
|
||||
assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue)
|
||||
assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedGroupNewEncoded.firstValue)
|
||||
|
||||
// expect Loki API DB migration to reflect new thread values:
|
||||
assertEquals("${OpenGroupApi.legacyDefaultServer}.oxen", capturedLokiLegacyGroup.firstValue)
|
||||
assertEquals("${OpenGroupApi.defaultServer}.oxen", capturedLokiNewGroup.firstValue)
|
||||
|
||||
assertEquals(newServer, lokiThreadOpenGroup.firstValue.server)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should migrate and delete legacy thread with conflicting new and old values`() {
|
||||
|
||||
// mock threadDB
|
||||
val capturedThreadId = argumentCaptor<Long>()
|
||||
val mockedThreadDb = mock<ThreadDatabase> {
|
||||
val legacyThreadRecord = legacyThreadRecord()
|
||||
val newThreadRecord = newThreadRecord()
|
||||
on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord)
|
||||
on { httpsOxenOpenGroups } doReturn listOf(newThreadRecord)
|
||||
on { deleteConversation(capturedThreadId.capture()) } doAnswer {}
|
||||
}
|
||||
|
||||
// mock groupDB
|
||||
val capturedGroupLegacyEncoded = argumentCaptor<String>()
|
||||
val mockedGroupDb = mock<GroupDatabase> {
|
||||
on { delete(capturedGroupLegacyEncoded.capture()) } doReturn true
|
||||
}
|
||||
|
||||
// mock LokiAPIDB
|
||||
val capturedLokiLegacyGroup = argumentCaptor<String>()
|
||||
val capturedLokiNewGroup = argumentCaptor<String>()
|
||||
val mockedLokiApi = mock<LokiAPIDatabase> {
|
||||
on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {}
|
||||
}
|
||||
|
||||
// mock messaging dbs
|
||||
val migrateMmsFromThreadId = argumentCaptor<Long>()
|
||||
val migrateMmsToThreadId = argumentCaptor<Long>()
|
||||
|
||||
val mockedMmsDb = mock<MmsDatabase> {
|
||||
on { migrateThreadId(migrateMmsFromThreadId.capture(), migrateMmsToThreadId.capture()) } doAnswer {}
|
||||
}
|
||||
|
||||
val migrateSmsFromThreadId = argumentCaptor<Long>()
|
||||
val migrateSmsToThreadId = argumentCaptor<Long>()
|
||||
val mockedSmsDb = mock<SmsDatabase> {
|
||||
on { migrateThreadId(migrateSmsFromThreadId.capture(), migrateSmsToThreadId.capture()) } doAnswer {}
|
||||
}
|
||||
|
||||
val lokiFromThreadId = argumentCaptor<Long>()
|
||||
val lokiToThreadId = argumentCaptor<Long>()
|
||||
val mockedLokiMessageDatabase = mock<LokiMessageDatabase> {
|
||||
on { migrateThreadId(lokiFromThreadId.capture(), lokiToThreadId.capture()) } doAnswer {}
|
||||
}
|
||||
|
||||
val mockedLokiThreadDb = mock<LokiThreadDatabase> {
|
||||
on { removeOpenGroupChat(eq(LEGACY_THREAD_ID)) } doAnswer {}
|
||||
}
|
||||
|
||||
val mockedDbComponent = mock<DatabaseComponent> {
|
||||
on { threadDatabase() } doReturn mockedThreadDb
|
||||
on { groupDatabase() } doReturn mockedGroupDb
|
||||
on { lokiAPIDatabase() } doReturn mockedLokiApi
|
||||
on { mmsDatabase() } doReturn mockedMmsDb
|
||||
on { smsDatabase() } doReturn mockedSmsDb
|
||||
on { lokiMessageDatabase() } doReturn mockedLokiMessageDatabase
|
||||
on { lokiThreadDatabase() } doReturn mockedLokiThreadDb
|
||||
}
|
||||
|
||||
OpenGroupMigrator.migrate(mockedDbComponent)
|
||||
|
||||
// should delete thread by thread ID
|
||||
assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue)
|
||||
|
||||
// should delete group by legacy encoded ID
|
||||
assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue)
|
||||
|
||||
// should migrate SMS from legacy thread ID to new thread ID
|
||||
assertEquals(LEGACY_THREAD_ID, migrateSmsFromThreadId.firstValue)
|
||||
assertEquals(NEW_THREAD_ID, migrateSmsToThreadId.firstValue)
|
||||
|
||||
// should migrate MMS from legacy thread ID to new thread ID
|
||||
assertEquals(LEGACY_THREAD_ID, migrateMmsFromThreadId.firstValue)
|
||||
assertEquals(NEW_THREAD_ID, migrateMmsToThreadId.firstValue)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user