mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-23 00:17:34 +00:00
Merge dev (#1468)
* Remove unused sizeResId * Fix caching * Prefix message with name in HomeActivity * Hide sender prefix for note to self * Hide sender prefix for control messages * Remove problematic getLastMessage() * Refactor snippet formatting * Remove unused RecoveryPhraseRestoreActivity * Fix unresolved theme attributes exception * Fix dialog button style * Investigation in progress * Working fix push before cleanup * Fixes #1346 * Removed unused logging imports * Put back some whitespace * Minor cleanup * Fix NPE on null display name * fix: disappearing viewmodel tests (#1432) * SES-1354 - Video call self viewer not mirrored (#1397) * Fixes #874 * Removed accidentally left in line * Fixed issue - push before cleanup * Cleaned up * Removed cruft --------- Co-authored-by: = <=> Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> * SES-1145 - New messages are hidden under keyboard - MK3 (#1415) * WIP * Working - push before cleanup * Fixes #1316 * Cleanup * PR review adjustments * Fixed scrolling when receiving an image based message while keyboard is up * Prevent auto-scroll to last seen item pos in conversation view if <= 3 * Put back <=3 check to scroll --------- Co-authored-by: = <=> Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> * Fix missing parenthesis * SES-789 - Scroll to bottom of long new message(s) (#1426) * WIP * Working - push before cleanup * Fixes #1316 * Cleanup * PR review adjustments * Fixed scrolling when receiving an image based message while keyboard is up * Prevent auto-scroll to last seen item pos in conversation view if <= 3 * Put back <=3 check to scroll * Forced scrolling to bottom of long messages (both sent and received) when already at the bottom of the RecyclerView * Fixes #1364 --------- Co-authored-by: = <=> Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> * SES-1352 - User and group names allowing multi-line strings (#1395) * Fix WIP * Resolved issue - pushing before cleanup & PR tomorrow morning * Enforced single line for new closed group names * Fixes #1394 * Final cleanup prior to PR * Added code to restore a previous contact nickname if an empty one is given * Added initial limits to nicknames and group names, both creation and display * Minor adjustments * Adjusted max nickname and group name to 35 chars as per Kee's instructions * Fixed closed group edit text able to get too wide and cut off buttons --------- Co-authored-by: = <=> Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> Co-authored-by: Al Lansley <al@oxen.io> * SES-212 - Always show delivery status of last sent message - FINAL! (#1418) * Fixes #1408 * Addressed PR feedback * Cleanup * PR adjustments * Further PR adjustments * Updated libsession-util * Added fix for crash when no messages * Ignoring dirty submodules so they don't show up in git * Re-fixed display of delivery status on last sent message (got broken by disappearing messages) * Removed ignore dirty modules line in .gitmodules as it all seems to be playing nice now --------- Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> Co-authored-by: Al Lansley <al@oxen.io> * fix: use a set for the from/to serialized lists (#1370) * Fixes #1347 (#1396) Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> Co-authored-by: Al Lansley <al@oxen.io> * SES-1156 - Ban and delete functionality fix (#1428) * WIP * Investigation in progress * End of day push * WIP * Fixes #1416 * Cleanup * Added code to remove zombie messages caught in limbo during a ban & delete - still chock full o' debug while finding root cause * Root cause debug WIP * Push prior to cleanup * Cleaned up for PR * fix: mms delete, remove unnecessary values from sms * Addressed PR feedback * fix: fix unit tests * Added '.run' folder with test setup * Update README.md Test commit for CI * Re-added accidentally removed closing brace --------- Co-authored-by: alansley <aclansley@gmail.com> Co-authored-by: Al Lansley <alansley@users.noreply.github.com> Co-authored-by: 0x330a <92654767+0x330a@users.noreply.github.com> * SES-1356 - List of recently used reaction emojis is not accurate (#1400) * WIP * Further WIP * Push prior to cleanup * Fixes #1015 * Added limiting to the count of recently used emoji that we store * Put back adjusted reaction pill layout to standard * Adjusted recently used reaction emojis already in list to go to start of list --------- Co-authored-by: = <=> * SES-697 - Add loading state when exporting logs (#1402) * WIP * Fixes #1401 * Cleanup from PR view * Final cleanup * Removed commented line of code & re-ordered comment * Addressed PR feedback * Re-allowed loading of avatars to throw exceptions rather than return null on failure --------- Co-authored-by: = <=> * SES-1251 - App crash on non alphanumeric first char search (#1393) * Investigation in progress * Working fix push before cleanup * Fixes #1346 * Removed unused logging imports * Put back some whitespace * Minor cleanup * Push before cleanup * Fixes #1346 - properly this time! * SES1567 - Community message delivery status fix (#1442) * Initial investigation * WIP * Continued work to track down cause of delivery status issue * Fixes #1438 * Cleanup for PR * Further cleanup * Fixed merge conflict * Addressed PR feedback --------- Co-authored-by: alansley <aclansley@gmail.com> * Tiny adjustment to center user name in Settings activity (#1446) * Addressed PR feedback * Cleanup * Initial fix implemented * Fixes #1448 * Addressed PR feedback * SES1688 - Deleting last message in conversation, group, or community leaves the RecyclerView in a broken state (#1449) * Initial fix implemented * Fixes #1448 * Addressed PR feedback * Handle case where there are no messages * build: update build number * Fix spacing when title is absent * Hide reply button in MessageDetails for group invitations * Remove reply from context menu for open group invitations * Ignore swipe reply to open group invitation * Fix multiple quote previews * Fix message menu icons not visible in light theme * Hide reply app bar menu item for open group invite * SES-1727 Mentions text is the wrong colour (#1454) * Fixes #1453 * Cleanup * Code review adjustments * Adjusted mentions to use the accent colour as their background colour when using light themes --------- Co-authored-by: alansley <aclansley@gmail.com> * Disable swipe to reply on open group invites * Fix multiple link previews * SES1718 - Message Sending Status (#1462) * Investigation in progress * Initial push for PR * Fixes #1461 * Removed leftover debug comments * Added minor optimisation to showMessageStatus method (bail early if the message isn't one we care about displaying details of to the user) * Minor cleanup * Tiny cleanup * Addressed PR feedback * Removed forgotten debug log line & forced delivery status elements to be removed on non-visible messages just in case * Minor refactor to simplify 'VisibleMessageView.showStatusMessage' --------- Co-authored-by: alansley <aclansley@gmail.com> * Fix margins * WIP * Commit before converting SmsDatabase from Java to Kotlin * Remove old expiration config strings from UpdateMessageBuilder * Fix group expiration update config messages * Fixed conversation view closing + hopefully wrong status text displayed + deletion of contact on removal of last message in 1-on-1 convo * Cleanup for PR review * Implemented PR feedback * Don't start expiration for group expiration update messages * Fix expiry update message for groups * Correctly don't start disappear timer on group timer updates * SES1813 - Fix regression test failures (#1473) * Initial fix for regression test failure 1.1 * Added permissions fix for sharing documents which should allow for thumbnail generation * Minor touch-up prior to merge into dev * Fixes #1813 * Fixes #1472 - please ignore previous fixes 1813 statement, I'd used the Jira ticket number rather than creating a GitHub issue and using that --------- Co-authored-by: alansley <aclansley@gmail.com> --------- Co-authored-by: alansley <aclansley@gmail.com> Co-authored-by: 0x330a <92654767+0x330a@users.noreply.github.com> Co-authored-by: Al Lansley <alansley@users.noreply.github.com> Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> Co-authored-by: Al Lansley <al@oxen.io>
This commit is contained in:
parent
7bcf823740
commit
d16faf94c9
24
.run/Run Tests.run.xml
Normal file
24
.run/Run Tests.run.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value="testPlayDebugUnitTestCoverageReport" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -31,8 +31,8 @@ configurations.all {
|
|||||||
exclude module: "commons-logging"
|
exclude module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 369
|
def canonicalVersionCode = 370
|
||||||
def canonicalVersionName = "1.18.1"
|
def canonicalVersionName = "1.18.2"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
@ -106,11 +107,6 @@
|
|||||||
android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.RecoveryPhraseRestoreActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.onboarding.LinkDeviceActivity"
|
android:name="org.thoughtcrime.securesms.onboarding.LinkDeviceActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
@ -230,11 +226,13 @@
|
|||||||
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar">
|
android:theme="@style/Theme.Session.DayNight.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
|
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
@ -478,9 +478,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
} catch (Exception exception) {
|
} catch (Exception e) {
|
||||||
// Do nothing
|
Log.e("Loki-Avatar", "Uploading avatar failed.");
|
||||||
Log.e("Loki-Avatar", "Uploading avatar failed", exception);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -39,15 +39,20 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
|||||||
public int getDesiredTheme() {
|
public int getDesiredTheme() {
|
||||||
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
|
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
|
||||||
int userSelectedTheme = themeState.getTheme();
|
int userSelectedTheme = themeState.getTheme();
|
||||||
|
|
||||||
|
// If the user has configured Session to follow the system light/dark theme mode then do so..
|
||||||
if (themeState.getFollowSystem()) {
|
if (themeState.getFollowSystem()) {
|
||||||
// do light or dark based on the selected theme
|
|
||||||
|
// Use light or dark versions of the user's theme based on light-mode / dark-mode settings
|
||||||
boolean isDayUi = UiModeUtilities.isDayUiMode(this);
|
boolean isDayUi = UiModeUtilities.isDayUiMode(this);
|
||||||
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
|
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
|
||||||
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
|
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
|
||||||
} else {
|
} else {
|
||||||
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
|
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else // ..otherwise just return their selected theme.
|
||||||
|
{
|
||||||
return userSelectedTheme;
|
return userSelectedTheme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.LinearLayout.VERTICAL
|
import android.widget.LinearLayout.VERTICAL
|
||||||
|
import android.widget.Space
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
@ -15,13 +16,11 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.setMargins
|
import androidx.core.view.setMargins
|
||||||
import androidx.core.view.setPadding
|
|
||||||
import androidx.core.view.updateMargins
|
import androidx.core.view.updateMargins
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
|
||||||
|
|
||||||
@DslMarker
|
@DslMarker
|
||||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
||||||
annotation class DialogDsl
|
annotation class DialogDsl
|
||||||
@ -31,13 +30,16 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
|
|
||||||
private val dp20 = toPx(20, context.resources)
|
private val dp20 = toPx(20, context.resources)
|
||||||
private val dp40 = toPx(40, context.resources)
|
private val dp40 = toPx(40, context.resources)
|
||||||
|
private val dp60 = toPx(60, context.resources)
|
||||||
|
|
||||||
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||||
|
|
||||||
private var dialog: AlertDialog? = null
|
private var dialog: AlertDialog? = null
|
||||||
private fun dismiss() = dialog?.dismiss()
|
private fun dismiss() = dialog?.dismiss()
|
||||||
|
|
||||||
private val topView = LinearLayout(context).apply { orientation = VERTICAL }
|
private val topView = LinearLayout(context)
|
||||||
|
.apply { setPadding(0, dp20, 0, 0) }
|
||||||
|
.apply { orientation = VERTICAL }
|
||||||
.also(dialogBuilder::setCustomTitle)
|
.also(dialogBuilder::setCustomTitle)
|
||||||
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||||
private val buttonLayout = LinearLayout(context)
|
private val buttonLayout = LinearLayout(context)
|
||||||
@ -53,18 +55,17 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
|
|
||||||
fun title(text: CharSequence?) = title(text?.toString())
|
fun title(text: CharSequence?) = title(text?.toString())
|
||||||
fun title(text: String?) {
|
fun title(text: String?) {
|
||||||
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) }
|
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
|
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
|
||||||
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
||||||
text(text, style) {
|
text(text, style) {
|
||||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||||
.apply { updateMargins(dp40, 0, dp40, dp20) }
|
.apply { updateMargins(dp40, 0, dp40, 0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
|
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
|
||||||
text ?: return
|
text ?: return
|
||||||
TextView(context, null, 0, style)
|
TextView(context, null, 0, style)
|
||||||
@ -73,6 +74,10 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
modify()
|
modify()
|
||||||
}.let(topView::addView)
|
}.let(topView::addView)
|
||||||
|
|
||||||
|
Space(context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, dp20)
|
||||||
|
}.let(topView::addView)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun view(view: View) = contentView.addView(view)
|
fun view(view: View) = contentView.addView(view)
|
||||||
@ -125,8 +130,7 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
) = Button(context, null, 0, style).apply {
|
) = Button(context, null, 0, style).apply {
|
||||||
setText(text)
|
setText(text)
|
||||||
contentDescription = resources.getString(contentDescriptionRes)
|
contentDescription = resources.getString(contentDescriptionRes)
|
||||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f)
|
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
|
||||||
.apply { setMargins(toPx(20, resources)) }
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
listener.invoke()
|
listener.invoke()
|
||||||
if (dismiss) dismiss()
|
if (dismiss) dismiss()
|
||||||
|
@ -184,16 +184,21 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
|
||||||
messagingDatabase.deleteMessage(messageID)
|
messagingDatabase.deleteMessage(messageID)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
||||||
|
|
||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
|
||||||
|
// Perform local delete
|
||||||
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
||||||
|
|
||||||
|
// Perform online delete
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
|
||||||
}
|
}
|
||||||
|
@ -93,6 +93,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if (intent?.action == ACTION_ANSWER) {
|
if (intent?.action == ACTION_ANSWER) {
|
||||||
val answerIntent = WebRtcCallService.acceptCallIntent(this)
|
val answerIntent = WebRtcCallService.acceptCallIntent(this)
|
||||||
|
answerIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||||
ContextCompat.startForegroundService(this, answerIntent)
|
ContextCompat.startForegroundService(this, answerIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +107,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
setShowWhenLocked(true)
|
setShowWhenLocked(true)
|
||||||
setTurnScreenOn(true)
|
setTurnScreenOn(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addFlags(
|
window.addFlags(
|
||||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||||
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||||
@ -334,6 +336,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
viewModel.localRenderer?.let { surfaceView ->
|
viewModel.localRenderer?.let { surfaceView ->
|
||||||
surfaceView.setZOrderOnTop(true)
|
surfaceView.setZOrderOnTop(true)
|
||||||
|
|
||||||
|
// Mirror the video preview of the person making the call to prevent disorienting them
|
||||||
|
surfaceView.setMirror(true)
|
||||||
|
|
||||||
binding.localRenderer.addView(surfaceView)
|
binding.localRenderer.addView(surfaceView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.DimenRes
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||||
@ -33,13 +32,12 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
var additionalDisplayName: String? = null
|
var additionalDisplayName: String? = null
|
||||||
var isLarge = false
|
var isLarge = false
|
||||||
|
|
||||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
private val profilePicturesCache = mutableMapOf<View, Recipient>()
|
||||||
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
|
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
constructor(context: Context, sender: Recipient): this(context) {
|
constructor(context: Context, sender: Recipient): this(context) {
|
||||||
@ -90,8 +88,8 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
val publicKey = publicKey ?: return
|
val publicKey = publicKey ?: return
|
||||||
val additionalPublicKey = additionalPublicKey
|
val additionalPublicKey = additionalPublicKey
|
||||||
if (additionalPublicKey != null) {
|
if (additionalPublicKey != null) {
|
||||||
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size)
|
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
|
||||||
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size)
|
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
|
||||||
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(binding.doubleModeImageView1)
|
glide.clear(binding.doubleModeImageView1)
|
||||||
@ -99,14 +97,14 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (additionalPublicKey == null && !isLarge) {
|
if (additionalPublicKey == null && !isLarge) {
|
||||||
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size)
|
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
|
||||||
binding.singleModeImageView.visibility = View.VISIBLE
|
binding.singleModeImageView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(binding.singleModeImageView)
|
glide.clear(binding.singleModeImageView)
|
||||||
binding.singleModeImageView.visibility = View.INVISIBLE
|
binding.singleModeImageView.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (additionalPublicKey == null && isLarge) {
|
if (additionalPublicKey == null && isLarge) {
|
||||||
setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size)
|
setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName)
|
||||||
binding.largeSingleModeImageView.visibility = View.VISIBLE
|
binding.largeSingleModeImageView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
glide.clear(binding.largeSingleModeImageView)
|
glide.clear(binding.largeSingleModeImageView)
|
||||||
@ -114,17 +112,19 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
|
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {
|
||||||
if (publicKey.isNotEmpty()) {
|
if (publicKey.isNotEmpty()) {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||||
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
if (profilePicturesCache[imageView] == recipient) return
|
||||||
|
profilePicturesCache[imageView] = recipient
|
||||||
val signalProfilePicture = recipient.contactPhoto
|
val signalProfilePicture = recipient.contactPhoto
|
||||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||||
|
|
||||||
|
glide.clear(imageView)
|
||||||
|
|
||||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||||
|
|
||||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.clear(imageView)
|
|
||||||
glide.load(signalProfilePicture)
|
glide.load(signalProfilePicture)
|
||||||
.placeholder(unknownRecipientDrawable)
|
.placeholder(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
@ -132,21 +132,19 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
|
} else if (recipient.isCommunityRecipient && recipient.groupAvatarId == null) {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(unknownOpenGroupDrawable)
|
glide.load(unknownOpenGroupDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
} else {
|
} else {
|
||||||
glide.clear(imageView)
|
|
||||||
glide.load(placeholder)
|
glide.load(placeholder)
|
||||||
.placeholder(unknownRecipientDrawable)
|
.placeholder(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
|
||||||
}
|
}
|
||||||
profilePicturesCache[publicKey] = recipient.profileAvatar
|
|
||||||
} else {
|
} else {
|
||||||
glide.load(unknownRecipientDrawable)
|
glide.load(unknownRecipientDrawable)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
|
||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@ -68,9 +67,7 @@ public class SearchToolbar extends LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onQueryTextChange(String newText) {
|
public boolean onQueryTextChange(String newText) { return onQueryTextSubmit(newText); }
|
||||||
return onQueryTextSubmit(newText);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||||
|
@ -3,133 +3,95 @@ package org.thoughtcrime.securesms.components.emoji;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.JsonUtil;
|
import org.session.libsignal.utilities.JsonUtil;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.LinkedList;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class RecentEmojiPageModel implements EmojiPageModel {
|
public class RecentEmojiPageModel implements EmojiPageModel {
|
||||||
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
||||||
private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji2";
|
public static final String RECENT_EMOJIS_KEY = "Recents";
|
||||||
private static final int EMOJI_LRU_SIZE = 50;
|
|
||||||
public static final String KEY = "Recents";
|
|
||||||
public static final List<String> DEFAULT_REACTIONS_LIST =
|
|
||||||
Arrays.asList("\ud83d\ude02", "\ud83e\udd70", "\ud83d\ude22", "\ud83d\ude21", "\ud83d\ude2e", "\ud83d\ude08");
|
|
||||||
|
|
||||||
private final SharedPreferences prefs;
|
public static final LinkedList<String> DEFAULT_REACTION_EMOJIS_LIST = new LinkedList<>(Arrays.asList(
|
||||||
private final LinkedHashSet<String> recentlyUsed;
|
"\ud83d\ude02",
|
||||||
|
"\ud83e\udd70",
|
||||||
|
"\ud83d\ude22",
|
||||||
|
"\ud83d\ude21",
|
||||||
|
"\ud83d\ude2e",
|
||||||
|
"\ud83d\ude08"));
|
||||||
|
|
||||||
|
public static final String DEFAULT_REACTION_EMOJIS_JSON_STRING = JsonUtil.toJson(new LinkedList<>(DEFAULT_REACTION_EMOJIS_LIST));
|
||||||
|
private static SharedPreferences prefs;
|
||||||
|
private static LinkedList<String> recentlyUsed;
|
||||||
|
|
||||||
public RecentEmojiPageModel(Context context) {
|
public RecentEmojiPageModel(Context context) {
|
||||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
this.recentlyUsed = getPersistedCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinkedHashSet<String> getPersistedCache() {
|
// Note: Do NOT try to populate or update the persisted recent emojis in the constructor - the
|
||||||
String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
|
// `getEmoji` method ends up getting called half-way through in a race-condition manner.
|
||||||
try {
|
|
||||||
CollectionType collectionType = TypeFactory.defaultInstance()
|
|
||||||
.constructCollectionType(LinkedHashSet.class, String.class);
|
|
||||||
return JsonUtil.getMapper().readValue(serialized, collectionType);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
return new LinkedHashSet<>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getKey() {
|
public String getKey() { return RECENT_EMOJIS_KEY; }
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int getIconAttr() {
|
@Override public int getIconAttr() { return R.attr.emoji_category_recent; }
|
||||||
return R.attr.emoji_category_recent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public List<String> getEmoji() {
|
@Override public List<String> getEmoji() {
|
||||||
List<String> recent = new ArrayList<>(recentlyUsed);
|
// Populate our recently used list if required (i.e., on first run)
|
||||||
List<String> out = new ArrayList<>(DEFAULT_REACTIONS_LIST.size());
|
if (recentlyUsed == null) {
|
||||||
|
try {
|
||||||
for (int i = 0; i < DEFAULT_REACTIONS_LIST.size(); i++) {
|
String recentlyUsedEmjoiJsonString = prefs.getString(RECENT_EMOJIS_KEY, DEFAULT_REACTION_EMOJIS_JSON_STRING);
|
||||||
if (recent.size() > i) {
|
recentlyUsed = JsonUtil.fromJson(recentlyUsedEmjoiJsonString, LinkedList.class);
|
||||||
out.add(recent.get(i));
|
} catch (Exception e) {
|
||||||
} else {
|
Log.w(TAG, e);
|
||||||
out.add(DEFAULT_REACTIONS_LIST.get(i));
|
Log.d(TAG, "Default reaction emoji data was corrupt (likely via key re-use on app upgrade) - rewriting fresh data.");
|
||||||
|
boolean writeSuccess = prefs.edit().putString(RECENT_EMOJIS_KEY, DEFAULT_REACTION_EMOJIS_JSON_STRING).commit();
|
||||||
|
if (!writeSuccess) { Log.w(TAG, "Failed to update recently used emojis in shared prefs."); }
|
||||||
|
recentlyUsed = DEFAULT_REACTION_EMOJIS_LIST;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return new ArrayList<>(recentlyUsed);
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public List<Emoji> getDisplayEmoji() {
|
@Override public List<Emoji> getDisplayEmoji() {
|
||||||
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public boolean hasSpriteMap() {
|
@Override public boolean hasSpriteMap() { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Uri getSpriteUri() {
|
public Uri getSpriteUri() { return null; }
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean isDynamic() {
|
@Override public boolean isDynamic() { return true; }
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCodePointSelected(String emoji) {
|
public static void onCodePointSelected(String emoji) {
|
||||||
recentlyUsed.remove(emoji);
|
// If the emoji is already in the recently used list then remove it..
|
||||||
recentlyUsed.add(emoji);
|
if (recentlyUsed.contains(emoji)) { recentlyUsed.removeFirstOccurrence(emoji); }
|
||||||
|
|
||||||
if (recentlyUsed.size() > EMOJI_LRU_SIZE) {
|
// ..and then regardless of whether the emoji used was already in the recently used list or not
|
||||||
Iterator<String> iterator = recentlyUsed.iterator();
|
// it gets placed as the first element in the list..
|
||||||
iterator.next();
|
recentlyUsed.addFirst(emoji);
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
final LinkedHashSet<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
|
// Ensure that we only ever store data for a maximum of 6 recently used emojis (this code will
|
||||||
new AsyncTask<Void, Void, Void>() {
|
// execute if if we did NOT remove any occurrence of a previously used emoji but then added the
|
||||||
|
// new emoji to the front of the list).
|
||||||
|
while (recentlyUsed.size() > 6) { recentlyUsed.removeLast(); }
|
||||||
|
|
||||||
@Override
|
// ..which we then save to shared prefs.
|
||||||
protected Void doInBackground(Void... params) {
|
String recentlyUsedAsJsonString = JsonUtil.toJson(recentlyUsed);
|
||||||
try {
|
boolean writeSuccess = prefs.edit().putString(RECENT_EMOJIS_KEY, recentlyUsedAsJsonString).commit();
|
||||||
String serialized = JsonUtil.toJsonThrows(latestRecentlyUsed);
|
if (!writeSuccess) { Log.w(TAG, "Failed to update recently used emojis in shared prefs."); }
|
||||||
prefs.edit()
|
|
||||||
.putString(EMOJI_LRU_PREFERENCE, serialized)
|
|
||||||
.apply();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] toReversePrimitiveArray(@NonNull LinkedHashSet<String> emojiSet) {
|
|
||||||
String[] emojis = new String[emojiSet.size()];
|
|
||||||
int i = emojiSet.size() - 1;
|
|
||||||
for (String emoji : emojiSet) {
|
|
||||||
emojis[i--] = emoji;
|
|
||||||
}
|
|
||||||
return emojis;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
@ -84,7 +85,7 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
|||||||
context.theme.resolveAttribute(item.iconRes, typedValue, true)
|
context.theme.resolveAttribute(item.iconRes, typedValue, true)
|
||||||
icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId))
|
icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId))
|
||||||
|
|
||||||
icon.imageTintList = color?.let(ColorStateList::valueOf)
|
icon.imageTintList = ColorStateList.valueOf(color ?: context.getColorFromAttr(android.R.attr.textColor))
|
||||||
}
|
}
|
||||||
item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it }
|
item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it }
|
||||||
title.setText(item.title)
|
title.setText(item.title)
|
||||||
|
@ -58,7 +58,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
|
|||||||
|
|
||||||
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
|
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
|
||||||
it.address.isOpenGroup
|
it.address.isCommunity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ class ConversationActionBarView @JvmOverloads constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (recipient.isGroupRecipient) {
|
if (recipient.isGroupRecipient) {
|
||||||
val title = if (recipient.isOpenGroupRecipient) {
|
val title = if (recipient.isCommunityRecipient) {
|
||||||
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
|
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
|
||||||
context.getString(R.string.ConversationActivity_active_member_count, userCount)
|
context.getString(R.string.ConversationActivity_active_member_count, userCount)
|
||||||
} else {
|
} else {
|
||||||
|
@ -68,7 +68,7 @@ enum class ExpiryType(
|
|||||||
AFTER_SEND(
|
AFTER_SEND(
|
||||||
ExpiryMode::AfterSend,
|
ExpiryMode::AfterSend,
|
||||||
R.string.expiration_type_disappear_after_send,
|
R.string.expiration_type_disappear_after_send,
|
||||||
R.string.expiration_type_disappear_after_read_description,
|
R.string.expiration_type_disappear_after_send_description,
|
||||||
R.string.AccessibilityId_disappear_after_send_option
|
R.string.AccessibilityId_disappear_after_send_option
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -35,11 +35,28 @@ class ContactListAdapter(
|
|||||||
binding.profilePictureView.update(contact.recipient)
|
binding.profilePictureView.update(contact.recipient)
|
||||||
binding.nameTextView.text = contact.displayName
|
binding.nameTextView.text = contact.displayName
|
||||||
binding.root.setOnClickListener { listener(contact.recipient) }
|
binding.root.setOnClickListener { listener(contact.recipient) }
|
||||||
|
|
||||||
|
// TODO: When we implement deleting contacts (hide might be safest for now) then probably set a long-click listener here w/ something like:
|
||||||
|
/*
|
||||||
|
binding.root.setOnLongClickListener {
|
||||||
|
Log.w("[ACL]", "Long clicked on contact ${contact.recipient.name}")
|
||||||
|
binding.contentView.context.showSessionDialog {
|
||||||
|
title("Delete Contact")
|
||||||
|
text("Are you sure you want to delete this contact?")
|
||||||
|
button(R.string.delete) {
|
||||||
|
val contacts = configFactory.contacts ?: return
|
||||||
|
contacts.upsertContact(contact.recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
cancelButton(::endActionMode)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() { binding.profilePictureView.recycle() }
|
||||||
binding.profilePictureView.recycle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HeaderViewHolder(
|
class HeaderViewHolder(
|
||||||
@ -52,15 +69,11 @@ class ContactListAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int { return items.size }
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
if (holder is ContactViewHolder) {
|
if (holder is ContactViewHolder) { holder.unbind() }
|
||||||
holder.unbind()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
@ -72,13 +85,9 @@ class ContactListAdapter(
|
|||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return if (viewType == ViewType.Contact) {
|
return if (viewType == ViewType.Contact) {
|
||||||
ContactViewHolder(
|
ContactViewHolder(ViewContactBinding.inflate(LayoutInflater.from(context), parent, false))
|
||||||
ViewContactBinding.inflate(LayoutInflater.from(context), parent, false)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
HeaderViewHolder(
|
HeaderViewHolder(ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false))
|
||||||
ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ import androidx.lifecycle.Observer
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
import androidx.lifecycle.flowWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@ -57,8 +56,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -106,6 +103,7 @@ import org.thoughtcrime.securesms.ApplicationContext
|
|||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
||||||
import org.thoughtcrime.securesms.audio.AudioRecorder
|
import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
||||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
|
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
|
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
|
||||||
@ -175,12 +173,11 @@ import org.thoughtcrime.securesms.util.MediaUtil
|
|||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||||
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
||||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||||
|
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.time.Instant
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@ -191,8 +188,6 @@ import kotlin.math.abs
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
private const val TAG = "ConversationActivityV2"
|
private const val TAG = "ConversationActivityV2"
|
||||||
|
|
||||||
@ -281,6 +276,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private val isScrolledToBottom: Boolean
|
private val isScrolledToBottom: Boolean
|
||||||
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
|
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
|
||||||
|
|
||||||
|
private val isScrolledToWithin30dpOfBottom: Boolean
|
||||||
|
get() = binding?.conversationRecyclerView?.isScrolledToWithin30dpOfBottom ?: true
|
||||||
|
|
||||||
private val layoutManager: LinearLayoutManager?
|
private val layoutManager: LinearLayoutManager?
|
||||||
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
|
||||||
|
|
||||||
@ -336,6 +334,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
lifecycleCoroutineScope = lifecycleScope
|
lifecycleCoroutineScope = lifecycleScope
|
||||||
)
|
)
|
||||||
adapter.visibleMessageViewDelegate = this
|
adapter.visibleMessageViewDelegate = this
|
||||||
|
|
||||||
|
// Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView if we're
|
||||||
|
// already near the the bottom and the data changes.
|
||||||
|
adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
|
||||||
|
|
||||||
adapter
|
adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +355,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private lateinit var reactionDelegate: ConversationReactionDelegate
|
private lateinit var reactionDelegate: ConversationReactionDelegate
|
||||||
private val reactWithAnyEmojiStartPage = -1
|
private val reactWithAnyEmojiStartPage = -1
|
||||||
|
|
||||||
|
// Properties for what message indices are visible previously & now, as well as the scroll state
|
||||||
|
private var previousLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
|
||||||
|
private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
|
||||||
|
private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE
|
||||||
|
|
||||||
// region Settings
|
// region Settings
|
||||||
companion object {
|
companion object {
|
||||||
// Extras
|
// Extras
|
||||||
@ -375,12 +383,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
||||||
setContentView(binding!!.root)
|
setContentView(binding!!.root)
|
||||||
|
|
||||||
// messageIdToScroll
|
// messageIdToScroll
|
||||||
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
||||||
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
||||||
val recipient = viewModel.recipient
|
val recipient = viewModel.recipient
|
||||||
val openGroup = recipient.let { viewModel.openGroup }
|
val openGroup = recipient.let { viewModel.openGroup }
|
||||||
if (recipient == null || (recipient.isOpenGroupRecipient && openGroup == null)) {
|
if (recipient == null || (recipient.isCommunityRecipient && openGroup == null)) {
|
||||||
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
|
||||||
return finish()
|
return finish()
|
||||||
}
|
}
|
||||||
@ -390,6 +399,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
setUpLinkPreviewObserver()
|
setUpLinkPreviewObserver()
|
||||||
restoreDraftIfNeeded()
|
restoreDraftIfNeeded()
|
||||||
setUpUiStateObserver()
|
setUpUiStateObserver()
|
||||||
|
|
||||||
binding!!.scrollToBottomButton.setOnClickListener {
|
binding!!.scrollToBottomButton.setOnClickListener {
|
||||||
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
|
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
|
||||||
val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
|
val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
|
||||||
@ -419,9 +429,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
setUpBlockedBanner()
|
setUpBlockedBanner()
|
||||||
binding!!.searchBottomBar.setEventListener(this)
|
binding!!.searchBottomBar.setEventListener(this)
|
||||||
updateSendAfterApprovalText()
|
updateSendAfterApprovalText()
|
||||||
showOrHideInputIfNeeded()
|
|
||||||
setUpMessageRequestsBar()
|
setUpMessageRequestsBar()
|
||||||
|
|
||||||
|
// Note: Do not `showOrHideInputIfNeeded` here - we'll never start this activity w/ the
|
||||||
|
// keyboard visible and have no need to immediately display it.
|
||||||
|
|
||||||
val weakActivity = WeakReference(this)
|
val weakActivity = WeakReference(this)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@ -563,17 +575,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
|
scrollToMostRecentMessageIfWeShould()
|
||||||
|
}
|
||||||
handleRecyclerViewScrolled()
|
handleRecyclerViewScrolled()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
recyclerScrollState = newState
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
||||||
showScrollToBottomButtonIfApplicable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun scrollToMostRecentMessageIfWeShould() {
|
||||||
|
// Grab an initial 'previous' last visible message..
|
||||||
|
if (previousLastVisibleRecyclerViewIndex == RecyclerView.NO_POSITION) {
|
||||||
|
previousLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..and grab the 'current' last visible message.
|
||||||
|
currentLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
|
||||||
|
|
||||||
|
// If the current last visible message index is less than the previous one (i.e. we've
|
||||||
|
// lost visibility of one or more messages due to showing the IME keyboard) AND we're
|
||||||
|
// at the bottom of the message feed..
|
||||||
|
val atBottomAndTrueLastNoLongerVisible = currentLastVisibleRecyclerViewIndex!! <= previousLastVisibleRecyclerViewIndex!! && !binding?.scrollToBottomButton?.isVisible!!
|
||||||
|
|
||||||
|
// ..OR we're at the last message or have received a new message..
|
||||||
|
val atLastOrReceivedNewMessage = currentLastVisibleRecyclerViewIndex == (adapter.itemCount - 1)
|
||||||
|
|
||||||
|
// ..then scroll the recycler view to the last message on resize. Note: We cannot just call
|
||||||
|
// scroll/smoothScroll - we have to `post` it or nothing happens!
|
||||||
|
if (atBottomAndTrueLastNoLongerVisible || atLastOrReceivedNewMessage) {
|
||||||
|
binding?.conversationRecyclerView?.post {
|
||||||
|
binding?.conversationRecyclerView?.smoothScrollToPosition(adapter.itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update our previous last visible view index to the current one
|
||||||
|
previousLastVisibleRecyclerViewIndex = currentLastVisibleRecyclerViewIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// called from onCreate
|
// called from onCreate
|
||||||
@ -760,13 +800,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// of the first unread message in the middle of the screen
|
// of the first unread message in the middle of the screen
|
||||||
if (isFirstLoad && !reverseMessageList) {
|
if (isFirstLoad && !reverseMessageList) {
|
||||||
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
|
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
|
||||||
|
|
||||||
if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
|
if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
|
||||||
|
|
||||||
return lastSeenItemPosition
|
return lastSeenItemPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
|
if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
|
||||||
|
|
||||||
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
|
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
|
||||||
return lastSeenItemPosition
|
return lastSeenItemPosition
|
||||||
}
|
}
|
||||||
@ -931,11 +970,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
view.glide = glide
|
view.glide = glide
|
||||||
view.onCandidateSelected = { handleMentionSelected(it) }
|
view.onCandidateSelected = { handleMentionSelected(it) }
|
||||||
additionalContentContainer.addView(view)
|
additionalContentContainer.addView(view)
|
||||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
|
||||||
this.mentionCandidatesView = view
|
this.mentionCandidatesView = view
|
||||||
view.show(candidates, viewModel.threadId)
|
view.show(candidates, viewModel.threadId)
|
||||||
} else {
|
} else {
|
||||||
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
|
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isCommunityRecipient)
|
||||||
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
this.mentionCandidatesView!!.setMentionCandidates(candidates)
|
||||||
}
|
}
|
||||||
isShowingMentionCandidatesView = true
|
isShowingMentionCandidatesView = true
|
||||||
@ -1040,8 +1079,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
private fun handleRecyclerViewScrolled() {
|
private fun handleRecyclerViewScrolled() {
|
||||||
val binding = binding ?: return
|
val binding = binding ?: return
|
||||||
|
|
||||||
|
// Note: The typing indicate is whether the other person / other people are typing - it has
|
||||||
|
// nothing to do with the IME keyboard state.
|
||||||
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
|
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
|
||||||
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
|
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
|
||||||
|
|
||||||
showScrollToBottomButtonIfApplicable()
|
showScrollToBottomButtonIfApplicable()
|
||||||
val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
|
val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
|
||||||
val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
|
val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
|
||||||
@ -1069,6 +1112,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val blindedRecipient = viewModel.blindedRecipient
|
val blindedRecipient = viewModel.blindedRecipient
|
||||||
val binding = binding ?: return
|
val binding = binding ?: return
|
||||||
val openGroup = viewModel.openGroup
|
val openGroup = viewModel.openGroup
|
||||||
|
|
||||||
val (textResource, insertParam) = when {
|
val (textResource, insertParam) = when {
|
||||||
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
||||||
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
|
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
|
||||||
@ -1148,7 +1192,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun copyOpenGroupUrl(thread: Recipient) {
|
override fun copyOpenGroupUrl(thread: Recipient) {
|
||||||
if (!thread.isOpenGroupRecipient) { return }
|
if (!thread.isCommunityRecipient) { return }
|
||||||
|
|
||||||
val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return
|
val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
||||||
@ -1207,6 +1251,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
// `position` is the adapter position; not the visual position
|
// `position` is the adapter position; not the visual position
|
||||||
private fun handleSwipeToReply(message: MessageRecord) {
|
private fun handleSwipeToReply(message: MessageRecord) {
|
||||||
|
if (message.isOpenGroupInvitation) return
|
||||||
val recipient = viewModel.recipient ?: return
|
val recipient = viewModel.recipient ?: return
|
||||||
binding?.inputBar?.draftQuote(recipient, message, glide)
|
binding?.inputBar?.draftQuote(recipient, message, glide)
|
||||||
}
|
}
|
||||||
@ -1286,6 +1331,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
sendEmojiRemoval(emoji, messageRecord)
|
sendEmojiRemoval(emoji, messageRecord)
|
||||||
} else {
|
} else {
|
||||||
sendEmojiReaction(emoji, messageRecord)
|
sendEmojiReaction(emoji, messageRecord)
|
||||||
|
RecentEmojiPageModel.onCodePointSelected(emoji) // Save to recently used reaction emojis
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1312,7 +1358,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
} else originalMessage.individualRecipient.address
|
} else originalMessage.individualRecipient.address
|
||||||
// Send it
|
// Send it
|
||||||
reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, true)
|
reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, true)
|
||||||
if (recipient.isOpenGroupRecipient) {
|
if (recipient.isCommunityRecipient) {
|
||||||
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
|
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
|
||||||
viewModel.openGroup?.let {
|
viewModel.openGroup?.let {
|
||||||
OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji)
|
OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji)
|
||||||
@ -1336,7 +1382,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
} else originalMessage.individualRecipient.address
|
} else originalMessage.individualRecipient.address
|
||||||
|
|
||||||
message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, false)
|
message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.serialize(), emoji, false)
|
||||||
if (recipient.isOpenGroupRecipient) {
|
if (recipient.isCommunityRecipient) {
|
||||||
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
|
val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return
|
||||||
viewModel.openGroup?.let {
|
viewModel.openGroup?.let {
|
||||||
OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji)
|
OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji)
|
||||||
@ -1733,7 +1779,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
sendAttachments(slideDeck.asAttachments(), body)
|
sendAttachments(slideDeck.asAttachments(), body)
|
||||||
}
|
}
|
||||||
INVITE_CONTACTS -> {
|
INVITE_CONTACTS -> {
|
||||||
if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
|
if (viewModel.recipient?.isCommunityRecipient != true) { return }
|
||||||
val extras = intent?.extras ?: return
|
val extras = intent?.extras ?: return
|
||||||
if (!intent.hasExtra(selectedContactsKey)) { return }
|
if (!intent.hasExtra(selectedContactsKey)) { return }
|
||||||
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
|
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
|
||||||
@ -1799,19 +1845,61 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
handleLongPress(messages.first(), 0) //TODO: begin selection mode
|
handleLongPress(messages.first(), 0) //TODO: begin selection mode
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messages: Set<MessageRecord>) {
|
// The option to "Delete just for me" or "Delete for everyone"
|
||||||
val recipient = viewModel.recipient ?: return
|
private fun showDeleteOrDeleteForEveryoneInCommunityUI(messages: Set<MessageRecord>) {
|
||||||
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
val bottomSheet = DeleteOptionsBottomSheet()
|
||||||
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
|
bottomSheet.recipient = viewModel.recipient!!
|
||||||
if (recipient.isOpenGroupRecipient) {
|
bottomSheet.onDeleteForMeTapped = {
|
||||||
val messageCount = 1
|
messages.forEach(viewModel::deleteLocally)
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
bottomSheet.onDeleteForEveryoneTapped = {
|
||||||
|
messages.forEach(viewModel::deleteForEveryone)
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
bottomSheet.onCancelTapped = {
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteLocallyUI(messages: Set<MessageRecord>) {
|
||||||
|
val messageCount = 1
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||||
button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
|
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
|
||||||
|
cancelButton(::endActionMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The messages in the provided set may be a single message, or multiple if there are a
|
||||||
|
// group of selected messages.
|
||||||
|
override fun deleteMessages(messages: Set<MessageRecord>) {
|
||||||
|
val recipient = viewModel.recipient
|
||||||
|
if (recipient == null) {
|
||||||
|
Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
||||||
|
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
|
||||||
|
|
||||||
|
// If the recipient is a community OR a Note-to-Self then we delete the message for everyone
|
||||||
|
if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
|
||||||
|
val messageCount = 1 // Only used for plurals string
|
||||||
|
showSessionDialog {
|
||||||
|
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||||
|
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||||
|
button(R.string.delete) {
|
||||||
|
messages.forEach(viewModel::deleteForEveryone); endActionMode()
|
||||||
|
}
|
||||||
cancelButton { endActionMode() }
|
cancelButton { endActionMode() }
|
||||||
}
|
}
|
||||||
|
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
|
||||||
} else if (allSentByCurrentUser && allHasHash) {
|
} else if (allSentByCurrentUser && allHasHash) {
|
||||||
val bottomSheet = DeleteOptionsBottomSheet()
|
val bottomSheet = DeleteOptionsBottomSheet()
|
||||||
bottomSheet.recipient = recipient
|
bottomSheet.recipient = recipient
|
||||||
@ -1830,13 +1918,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||||
} else {
|
}
|
||||||
|
else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
|
||||||
|
{
|
||||||
val messageCount = 1
|
val messageCount = 1
|
||||||
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||||
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
|
button(R.string.delete) {
|
||||||
|
messages.forEach(viewModel::deleteLocally); endActionMode()
|
||||||
|
}
|
||||||
cancelButton(::endActionMode)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1855,7 +1946,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.ConversationFragment_ban_selected_user)
|
title(R.string.ConversationFragment_ban_selected_user)
|
||||||
text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
|
text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
|
||||||
button(R.string.ban) { viewModel.banAndDeleteAll(messages.first().individualRecipient); endActionMode() }
|
button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
|
||||||
cancelButton(::endActionMode)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1937,7 +2028,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val message = messages.first() as MmsMessageRecord
|
val message = messages.first() as MmsMessageRecord
|
||||||
|
|
||||||
// Do not allow the user to download a file attachment before it has finished downloading
|
// Do not allow the user to download a file attachment before it has finished downloading
|
||||||
// TODO: Localise the msg in this toast!
|
|
||||||
if (message.isMediaPending) {
|
if (message.isMediaPending) {
|
||||||
Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
@ -2107,4 +2197,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdapterDataObserver implementation to scroll us to the bottom of the ConversationRecyclerView
|
||||||
|
// when we're already near the bottom and we send or receive a message.
|
||||||
|
inner class ConversationAdapterDataObserver(val recyclerView: ConversationRecyclerView, val adapter: ConversationAdapter) : RecyclerView.AdapterDataObserver() {
|
||||||
|
override fun onChanged() {
|
||||||
|
super.onChanged()
|
||||||
|
if (recyclerView.isScrolledToWithin30dpOfBottom) {
|
||||||
|
// Note: The adapter itemCount is zero based - so calling this with the itemCount in
|
||||||
|
// a non-zero based manner scrolls us to the bottom of the last message (including
|
||||||
|
// to the bottom of long messages as required by Jira SES-789 / GitHub 1364).
|
||||||
|
recyclerView.scrollToPosition(adapter.itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -22,10 +22,12 @@ import kotlinx.coroutines.launch
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
@ -57,6 +59,7 @@ class ConversationAdapter(
|
|||||||
private val contactCache = SparseArray<Contact>(100)
|
private val contactCache = SparseArray<Contact>(100)
|
||||||
private val contactLoadedCache = SparseBooleanArray(100)
|
private val contactLoadedCache = SparseBooleanArray(100)
|
||||||
private val lastSeen = AtomicLong(originalLastSeen)
|
private val lastSeen = AtomicLong(originalLastSeen)
|
||||||
|
private var lastSentMessageId: Long = -1L
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycleCoroutineScope.launch(IO) {
|
lifecycleCoroutineScope.launch(IO) {
|
||||||
@ -136,7 +139,8 @@ class ConversationAdapter(
|
|||||||
senderId,
|
senderId,
|
||||||
lastSeen.get(),
|
lastSeen.get(),
|
||||||
visibleMessageViewDelegate,
|
visibleMessageViewDelegate,
|
||||||
onAttachmentNeedsDownload
|
onAttachmentNeedsDownload,
|
||||||
|
lastSentMessageId
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!message.isDeleted) {
|
if (!message.isDeleted) {
|
||||||
@ -205,8 +209,23 @@ class ConversationAdapter(
|
|||||||
return messageDB.readerFor(cursor).current
|
return messageDB.readerFor(cursor).current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getLastSentMessageId(cursor: Cursor): Long {
|
||||||
|
// If we don't move to first (or at least step backwards) we can step off the end of the
|
||||||
|
// cursor and any query will return an "Index = -1" error.
|
||||||
|
val cursorHasContent = cursor.moveToFirst()
|
||||||
|
if (cursorHasContent) {
|
||||||
|
val thisThreadId = cursor.getLong(4) // Column index 4 is "thread_id"
|
||||||
|
if (thisThreadId != -1L) {
|
||||||
|
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
return messageDB.getLastSentMessageFromSender(thisThreadId, thisUsersSessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1L
|
||||||
|
}
|
||||||
|
|
||||||
override fun changeCursor(cursor: Cursor?) {
|
override fun changeCursor(cursor: Cursor?) {
|
||||||
super.changeCursor(cursor)
|
super.changeCursor(cursor)
|
||||||
|
|
||||||
val toRemove = mutableSetOf<MessageRecord>()
|
val toRemove = mutableSetOf<MessageRecord>()
|
||||||
val toDeselect = mutableSetOf<Pair<Int, MessageRecord>>()
|
val toDeselect = mutableSetOf<Pair<Int, MessageRecord>>()
|
||||||
for (selected in selectedItems) {
|
for (selected in selectedItems) {
|
||||||
@ -224,6 +243,11 @@ class ConversationAdapter(
|
|||||||
toDeselect.iterator().forEach { (pos, record) ->
|
toDeselect.iterator().forEach { (pos, record) ->
|
||||||
onDeselect(record, pos)
|
onDeselect(record, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This value gets updated here ONLY when the cursor changes, and the value is then passed
|
||||||
|
// through to `VisibleMessageView.bind` each time we bind via `onBindItemViewHolder`, above.
|
||||||
|
// If there are no messages then lastSentMessageId is assigned the value -1L.
|
||||||
|
if (cursor != null) { lastSentMessageId = getLastSentMessageId(cursor) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
|
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
|
||||||
|
@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.components.menu.ActionItem
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
|
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
|
||||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
|
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||||
@ -533,7 +532,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
|
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
|
||||||
// Reply
|
// Reply
|
||||||
val canWrite = openGroup == null || openGroup.canWrite
|
val canWrite = openGroup == null || openGroup.canWrite
|
||||||
if (canWrite && !message.isPending && !message.isFailed) {
|
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
|
||||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
|
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
|
||||||
}
|
}
|
||||||
// Copy message text
|
// Copy message text
|
||||||
@ -541,7 +540,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
||||||
}
|
}
|
||||||
// Copy Session ID
|
// Copy Session ID
|
||||||
if (recipient.isGroupRecipient && !recipient.isOpenGroupRecipient && message.recipient.address.toString() != userPublicKey) {
|
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
|
||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
|
||||||
}
|
}
|
||||||
// Delete message
|
// Delete message
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
@ -22,9 +27,12 @@ import org.session.libsession.utilities.Address
|
|||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class ConversationViewModel(
|
class ConversationViewModel(
|
||||||
@ -144,9 +152,14 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
|
||||||
val recipient = recipient ?: return@launch
|
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
|
||||||
|
|
||||||
repository.deleteForEveryone(threadId, recipient, message)
|
repository.deleteForEveryone(threadId, recipient, message)
|
||||||
|
.onSuccess {
|
||||||
|
Log.d("Loki", "Deleted message ${message.id} ")
|
||||||
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
|
Log.w("Loki", "FAILED TO delete message ${message.id} ")
|
||||||
showMessage("Couldn't delete message due to error: $it")
|
showMessage("Couldn't delete message due to error: $it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,10 +181,15 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch {
|
fun banAndDeleteAll(messageRecord: MessageRecord) = viewModelScope.launch {
|
||||||
repository.banAndDeleteAll(threadId, recipient)
|
|
||||||
|
repository.banAndDeleteAll(threadId, messageRecord.individualRecipient)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
|
// At this point the server side messages have been successfully deleted..
|
||||||
showMessage("Successfully banned user and deleted all their messages")
|
showMessage("Successfully banned user and deleted all their messages")
|
||||||
|
|
||||||
|
// ..so we can now delete all their messages in this thread from local storage & remove the views.
|
||||||
|
repository.deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
showMessage("Couldn't execute request due to error: $it")
|
showMessage("Couldn't execute request due to error: $it")
|
||||||
|
@ -123,7 +123,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
AppTheme {
|
AppTheme {
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
state = state,
|
state = state,
|
||||||
onReply = { setResultAndFinish(ON_REPLY) },
|
onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
|
||||||
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
||||||
onDelete = { setResultAndFinish(ON_DELETE) },
|
onDelete = { setResultAndFinish(ON_DELETE) },
|
||||||
onClickImage = { viewModel.onClickImage(it) },
|
onClickImage = { viewModel.onClickImage(it) },
|
||||||
@ -145,7 +145,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MessageDetails(
|
fun MessageDetails(
|
||||||
state: MessageDetailsState,
|
state: MessageDetailsState,
|
||||||
onReply: () -> Unit = {},
|
onReply: (() -> Unit)? = null,
|
||||||
onResend: (() -> Unit)? = null,
|
onResend: (() -> Unit)? = null,
|
||||||
onDelete: () -> Unit = {},
|
onDelete: () -> Unit = {},
|
||||||
onClickImage: (Int) -> Unit = {},
|
onClickImage: (Int) -> Unit = {},
|
||||||
@ -214,18 +214,20 @@ fun CellMetadata(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CellButtons(
|
fun CellButtons(
|
||||||
onReply: () -> Unit = {},
|
onReply: (() -> Unit)? = null,
|
||||||
onResend: (() -> Unit)? = null,
|
onResend: (() -> Unit)? = null,
|
||||||
onDelete: () -> Unit = {},
|
onDelete: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Cell {
|
Cell {
|
||||||
Column {
|
Column {
|
||||||
|
onReply?.let {
|
||||||
ItemButton(
|
ItemButton(
|
||||||
stringResource(R.string.reply),
|
stringResource(R.string.reply),
|
||||||
R.drawable.ic_message_details__reply,
|
R.drawable.ic_message_details__reply,
|
||||||
onClick = onReply
|
onClick = it
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
|
}
|
||||||
onResend?.let {
|
onResend?.let {
|
||||||
ItemButton(
|
ItemButton(
|
||||||
stringResource(R.string.resend),
|
stringResource(R.string.resend),
|
||||||
|
@ -117,7 +117,7 @@ class MessageDetailsViewModel @Inject constructor(
|
|||||||
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
|
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
|
||||||
|
|
||||||
fun onClickImage(index: Int) {
|
fun onClickImage(index: Int) {
|
||||||
val state = state.value ?: return
|
val state = state.value
|
||||||
val mmsRecord = state.mmsRecord ?: return
|
val mmsRecord = state.mmsRecord ?: return
|
||||||
val slide = mmsRecord.slideDeck.slides[index] ?: return
|
val slide = mmsRecord.slideDeck.slides[index] ?: return
|
||||||
// only open to downloaded images
|
// only open to downloaded images
|
||||||
@ -158,6 +158,7 @@ data class MessageDetailsState(
|
|||||||
val thread: Recipient? = null,
|
val thread: Recipient? = null,
|
||||||
) {
|
) {
|
||||||
val fromTitle = GetString(R.string.message_details_header__from)
|
val fromTitle = GetString(R.string.message_details_header__from)
|
||||||
|
val canReply = record?.isOpenGroupInvitation != true
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Attachment(
|
data class Attachment(
|
||||||
|
@ -140,6 +140,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
|
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
|
||||||
|
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
|
||||||
|
|
||||||
quote = message
|
quote = message
|
||||||
|
|
||||||
// If we already have a link preview View then clear the 'additional content' layout so that
|
// If we already have a link preview View then clear the 'additional content' layout so that
|
||||||
@ -178,7 +180,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
// message we'll bail early if a link preview View already exists and just let
|
// message we'll bail early if a link preview View already exists and just let
|
||||||
// `updateLinkPreview` get called to update the existing View.
|
// `updateLinkPreview` get called to update the existing View.
|
||||||
if (linkPreview != null && linkPreviewDraftView != null) return
|
if (linkPreview != null && linkPreviewDraftView != null) return
|
||||||
|
linkPreviewDraftView?.let(binding.inputBarAdditionalContentContainer::removeView)
|
||||||
linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this }
|
linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this }
|
||||||
|
|
||||||
// Add the link preview View. Note: If there's already a quote View in the 'additional
|
// Add the link preview View. Note: If there's already a quote View in the 'additional
|
||||||
|
@ -65,7 +65,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
||||||
// Copy Session ID
|
// Copy Session ID
|
||||||
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
||||||
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
(thread.isGroupRecipient && !thread.isCommunityRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
||||||
// Message detail
|
// Message detail
|
||||||
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
|
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
|
||||||
// Resend
|
// Resend
|
||||||
@ -77,7 +77,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
||||||
// Reply
|
// Reply
|
||||||
menu.findItem(R.id.menu_context_reply).isVisible =
|
menu.findItem(R.id.menu_context_reply).isVisible =
|
||||||
(selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed)
|
(selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed && !firstMessage.isOpenGroupInvitation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {
|
||||||
|
@ -50,7 +50,7 @@ object ConversationMenuHelper {
|
|||||||
) {
|
) {
|
||||||
// Prepare
|
// Prepare
|
||||||
menu.clear()
|
menu.clear()
|
||||||
val isOpenGroup = thread.isOpenGroupRecipient
|
val isOpenGroup = thread.isCommunityRecipient
|
||||||
// Base menu (options that should always be present)
|
// Base menu (options that should always be present)
|
||||||
inflater.inflate(R.menu.menu_conversation, menu)
|
inflater.inflate(R.menu.menu_conversation, menu)
|
||||||
// Expiring messages
|
// Expiring messages
|
||||||
@ -253,7 +253,7 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
|
private fun copyOpenGroupUrl(context: Context, thread: Recipient) {
|
||||||
if (!thread.isOpenGroupRecipient) { return }
|
if (!thread.isCommunityRecipient) { return }
|
||||||
val listener = context as? ConversationMenuListener ?: return
|
val listener = context as? ConversationMenuListener ?: return
|
||||||
listener.copyOpenGroupUrl(thread)
|
listener.copyOpenGroupUrl(thread)
|
||||||
}
|
}
|
||||||
@ -300,7 +300,7 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun inviteContacts(context: Context, thread: Recipient) {
|
private fun inviteContacts(context: Context, thread: Recipient) {
|
||||||
if (!thread.isOpenGroupRecipient) { return }
|
if (!thread.isCommunityRecipient) { return }
|
||||||
val intent = Intent(context, SelectContactsActivity::class.java)
|
val intent = Intent(context, SelectContactsActivity::class.java)
|
||||||
val activity = context as AppCompatActivity
|
val activity = context as AppCompatActivity
|
||||||
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)
|
activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS)
|
||||||
|
@ -32,10 +32,12 @@ import org.session.libsession.messaging.contacts.Contact
|
|||||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.ViewUtil
|
import org.session.libsession.utilities.ViewUtil
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.session.libsession.utilities.modifyLayoutParams
|
import org.session.libsession.utilities.modifyLayoutParams
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||||
@ -64,6 +66,7 @@ private const val TAG = "VisibleMessageView"
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class VisibleMessageView : LinearLayout {
|
class VisibleMessageView : LinearLayout {
|
||||||
|
private var replyDisabled: Boolean = false
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
||||||
@Inject lateinit var lokiApiDb: LokiAPIDatabase
|
@Inject lateinit var lokiApiDb: LokiAPIDatabase
|
||||||
@ -131,8 +134,10 @@ class VisibleMessageView : LinearLayout {
|
|||||||
senderSessionID: String,
|
senderSessionID: String,
|
||||||
lastSeen: Long,
|
lastSeen: Long,
|
||||||
delegate: VisibleMessageViewDelegate? = null,
|
delegate: VisibleMessageViewDelegate? = null,
|
||||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
||||||
|
lastSentMessageId: Long
|
||||||
) {
|
) {
|
||||||
|
replyDisabled = message.isOpenGroupInvitation
|
||||||
val threadID = message.threadId
|
val threadID = message.threadId
|
||||||
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
||||||
val isGroupThread = thread.isGroupRecipient
|
val isGroupThread = thread.isGroupRecipient
|
||||||
@ -164,7 +169,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
binding.profilePictureView.publicKey = senderSessionID
|
binding.profilePictureView.publicKey = senderSessionID
|
||||||
binding.profilePictureView.update(message.individualRecipient)
|
binding.profilePictureView.update(message.individualRecipient)
|
||||||
binding.profilePictureView.setOnClickListener {
|
binding.profilePictureView.setOnClickListener {
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isCommunityRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
||||||
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
||||||
// TODO: support v2 soon
|
// TODO: support v2 soon
|
||||||
@ -177,7 +182,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
maybeShowUserDetails(senderSessionID, threadID)
|
maybeShowUserDetails(senderSessionID, threadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isCommunityRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||||
var standardPublicKey = ""
|
var standardPublicKey = ""
|
||||||
var blindedPublicKey: String? = null
|
var blindedPublicKey: String? = null
|
||||||
@ -193,16 +198,20 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
|
binding.senderNameTextView.isVisible = !message.isOutgoing && (isStartOfMessageCluster && (isGroupThread || snIsSelected))
|
||||||
val contactContext =
|
val contactContext =
|
||||||
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
if (thread.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||||
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
|
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
|
||||||
|
|
||||||
// Unread marker
|
// Unread marker
|
||||||
binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
binding.unreadMarkerContainer.isVisible = lastSeen != -1L && message.timestamp > lastSeen && (previous == null || previous.timestamp <= lastSeen) && !message.isOutgoing
|
||||||
|
|
||||||
// Date break
|
// Date break
|
||||||
val showDateBreak = isStartOfMessageCluster || snIsSelected
|
val showDateBreak = isStartOfMessageCluster || snIsSelected
|
||||||
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
||||||
binding.dateBreakTextView.isVisible = showDateBreak
|
binding.dateBreakTextView.isVisible = showDateBreak
|
||||||
// Message status indicator
|
|
||||||
|
// Update message status indicator
|
||||||
showStatusMessage(message)
|
showStatusMessage(message)
|
||||||
|
|
||||||
// Emoji Reactions
|
// Emoji Reactions
|
||||||
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
|
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
|
||||||
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||||
@ -237,42 +246,101 @@ class VisibleMessageView : LinearLayout {
|
|||||||
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
|
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to display or hide the status of a message.
|
||||||
|
// Note: Although most commonly used to display the delivery status of a message, we also use the
|
||||||
|
// message status area to display the disappearing messages state - so in this latter case we'll
|
||||||
|
// be displaying the "Sent" and the animating clock icon for outgoing messages or "Read" and the
|
||||||
|
// animated clock icon for incoming messages.
|
||||||
private fun showStatusMessage(message: MessageRecord) {
|
private fun showStatusMessage(message: MessageRecord) {
|
||||||
val disappearing = message.expiresIn > 0
|
// We'll start by hiding everything and then only make visible what we need
|
||||||
|
binding.messageStatusTextView.isVisible = false
|
||||||
|
binding.messageStatusImageView.isVisible = false
|
||||||
|
binding.expirationTimerView.isVisible = false
|
||||||
|
|
||||||
|
// Get details regarding how we should display the message (it's delivery icon, icon tint colour, and
|
||||||
|
// the resource string for what text to display (R.string.delivery_status_sent etc.).
|
||||||
|
val (iconID, iconColor, textId) = getMessageStatusInfo(message)
|
||||||
|
|
||||||
|
// If we get any nulls then a message isn't one with a state that we care about (i.e., control messages
|
||||||
|
// etc.) - so bail. See: `DisplayRecord.is<WHATEVER>` for the full suite of message state methods.
|
||||||
|
// Also: We set all delivery status elements visibility to false just to make sure we don't display any
|
||||||
|
// stale data.
|
||||||
|
if (textId == null) return
|
||||||
|
|
||||||
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
|
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
|
||||||
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.statusContainer.modifyLayoutParams<ConstraintLayout.LayoutParams> {
|
binding.statusContainer.modifyLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
horizontalBias = if (message.isOutgoing) 1f else 0f
|
horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.expirationTimerView.isGone = true
|
// If the message is incoming AND it is not scheduled to disappear then don't show any status or timer details
|
||||||
|
val scheduledToDisappear = message.expiresIn > 0
|
||||||
|
if (message.isIncoming && !scheduledToDisappear) return
|
||||||
|
|
||||||
if (message.isOutgoing || disappearing) {
|
// Set text & icons as appropriate for the message state. Note: Possible message states we care
|
||||||
val (iconID, iconColor, textId) = getMessageStatusImage(message)
|
// about are: isFailed, isSyncFailed, isPending, isSyncing, isResyncing, isRead, and isSent.
|
||||||
textId?.let(binding.messageStatusTextView::setText)
|
textId.let(binding.messageStatusTextView::setText)
|
||||||
iconColor?.let(binding.messageStatusTextView::setTextColor)
|
iconColor?.let(binding.messageStatusTextView::setTextColor)
|
||||||
iconID?.let { ContextCompat.getDrawable(context, it) }
|
iconID?.let { ContextCompat.getDrawable(context, it) }
|
||||||
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
|
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
|
||||||
?.let(binding.messageStatusImageView::setImageDrawable)
|
?.let(binding.messageStatusImageView::setImageDrawable)
|
||||||
|
|
||||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
// Potential options at this point are that the message is:
|
||||||
val isLastMessage = message.id == lastMessageID
|
// i.) incoming AND scheduled to disappear.
|
||||||
binding.messageStatusTextView.isVisible =
|
// ii.) outgoing but NOT scheduled to disappear, or
|
||||||
textId != null && (!message.isSent || isLastMessage || disappearing)
|
// iii.) outgoing AND scheduled to disappear.
|
||||||
val showTimer = disappearing && !message.isPending
|
|
||||||
binding.messageStatusImageView.isVisible =
|
|
||||||
iconID != null && !showTimer && (!message.isSent || isLastMessage)
|
|
||||||
|
|
||||||
binding.messageStatusImageView.bringToFront()
|
// ----- Case i..) Message is incoming and scheduled to disappear -----
|
||||||
|
if (message.isIncoming && scheduledToDisappear) {
|
||||||
|
// Display the status ('Read') and the show the timer only (no delivery icon)
|
||||||
|
binding.messageStatusTextView.isVisible = true
|
||||||
|
binding.expirationTimerView.isVisible = true
|
||||||
binding.expirationTimerView.bringToFront()
|
binding.expirationTimerView.bringToFront()
|
||||||
binding.expirationTimerView.isVisible = showTimer
|
updateExpirationTimer(message)
|
||||||
if (showTimer) updateExpirationTimer(message)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- If we got here then we know the message is outgoing ---
|
||||||
|
|
||||||
|
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
|
||||||
|
val isLastSentMessage = lastSentMessageId == message.id
|
||||||
|
|
||||||
|
// ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
|
||||||
|
if (!scheduledToDisappear) {
|
||||||
|
// If this isn't a disappearing message then we never show the timer
|
||||||
|
|
||||||
|
// If the message has NOT been successfully sent then always show the delivery status text and icon..
|
||||||
|
val neitherSentNorRead = !(message.isSent || message.isRead)
|
||||||
|
if (neitherSentNorRead) {
|
||||||
|
binding.messageStatusTextView.isVisible = true
|
||||||
|
binding.messageStatusImageView.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
binding.messageStatusTextView.isVisible = false
|
// ..but if the message HAS been successfully sent or read then only display the delivery status
|
||||||
binding.messageStatusImageView.isVisible = false
|
// text and image if this is the last sent message.
|
||||||
|
binding.messageStatusTextView.isVisible = isLastSentMessage
|
||||||
|
binding.messageStatusImageView.isVisible = isLastSentMessage
|
||||||
|
if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----
|
||||||
|
{
|
||||||
|
// Always display the delivery status text on all outgoing disappearing messages
|
||||||
|
binding.messageStatusTextView.isVisible = true
|
||||||
|
|
||||||
|
// If the message is sent or has been read..
|
||||||
|
val sentOrRead = message.isSent || message.isRead
|
||||||
|
if (sentOrRead) {
|
||||||
|
// ..then display the timer icon for this disappearing message (but keep the message status icon hidden)
|
||||||
|
binding.expirationTimerView.isVisible = true
|
||||||
|
binding.expirationTimerView.bringToFront()
|
||||||
|
updateExpirationTimer(message)
|
||||||
|
} else {
|
||||||
|
// If the message has NOT been sent or read (or it has failed) then show the delivery status icon rather than the timer icon
|
||||||
|
binding.messageStatusImageView.isVisible = true
|
||||||
|
binding.messageStatusImageView.bringToFront()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,10 +362,9 @@ class VisibleMessageView : LinearLayout {
|
|||||||
@ColorInt val iconTint: Int?,
|
@ColorInt val iconTint: Int?,
|
||||||
@StringRes val messageText: Int?)
|
@StringRes val messageText: Int?)
|
||||||
|
|
||||||
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
|
private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo = when {
|
||||||
message.isFailed ->
|
message.isFailed ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(R.drawable.ic_delivery_status_failed,
|
||||||
R.drawable.ic_delivery_status_failed,
|
|
||||||
resources.getColor(R.color.destructive, context.theme),
|
resources.getColor(R.color.destructive, context.theme),
|
||||||
R.string.delivery_status_failed
|
R.string.delivery_status_failed
|
||||||
)
|
)
|
||||||
@ -310,24 +377,32 @@ class VisibleMessageView : LinearLayout {
|
|||||||
message.isPending ->
|
message.isPending ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.delivery_status_sending
|
||||||
)
|
)
|
||||||
message.isResyncing ->
|
message.isSyncing || message.isResyncing ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.delivery_status_sending // We COULD tell the user that we're `syncing` (R.string.delivery_status_syncing) but it will likely make more sense to them if we say "Sending"
|
||||||
)
|
)
|
||||||
message.isRead || !message.isOutgoing ->
|
message.isRead || message.isIncoming ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_read,
|
R.drawable.ic_delivery_status_read,
|
||||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.delivery_status_read
|
||||||
)
|
)
|
||||||
else ->
|
message.isSent ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sent,
|
R.drawable.ic_delivery_status_sent,
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
R.string.delivery_status_sent
|
R.string.delivery_status_sent
|
||||||
)
|
)
|
||||||
|
else -> {
|
||||||
|
// The message isn't one we care about for message statuses we display to the user (i.e.,
|
||||||
|
// control messages etc. - see the `DisplayRecord.is<WHATEVER>` suite of methods for options).
|
||||||
|
MessageStatusInfo(null, null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExpirationTimer(message: MessageRecord) {
|
private fun updateExpirationTimer(message: MessageRecord) {
|
||||||
@ -401,6 +476,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
} else {
|
} else {
|
||||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||||
}
|
}
|
||||||
|
if (replyDisabled) return
|
||||||
if (translationX > 0) { return } // Only allow swipes to the left
|
if (translationX > 0) { return } // Only allow swipes to the left
|
||||||
// The idea here is to asymptotically approach a maximum drag distance
|
// The idea here is to asymptotically approach a maximum drag distance
|
||||||
val damping = 50.0f
|
val damping = 50.0f
|
||||||
|
@ -241,7 +241,21 @@ public class AttachmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void selectDocument(Activity activity, int requestCode) {
|
public static void selectDocument(Activity activity, int requestCode) {
|
||||||
selectMediaType(activity, "*/*", null, requestCode);
|
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
||||||
|
|
||||||
|
// The READ_EXTERNAL_STORAGE permission is deprecated (and will AUTO-FAIL if requested!) on
|
||||||
|
// Android 13 and above (API 33 - 'Tiramisu') we must ask for READ_MEDIA_VIDEO/IMAGES/AUDIO instead.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
||||||
|
.request(Manifest.permission.READ_MEDIA_IMAGES)
|
||||||
|
.request(Manifest.permission.READ_MEDIA_AUDIO);
|
||||||
|
} else {
|
||||||
|
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||||
|
}
|
||||||
|
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||||
|
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
|
||||||
|
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
|
import android.text.style.BackgroundColorSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.util.Range
|
import android.util.Range
|
||||||
|
import androidx.appcompat.widget.ThemeUtils
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.combine.Tuple2
|
import nl.komponents.kovenant.combine.Tuple2
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
|
import org.thoughtcrime.securesms.util.getColorResourceIdFromAttr
|
||||||
|
import org.thoughtcrime.securesms.util.getMessageTextColourAttr
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
object MentionUtilities {
|
object MentionUtilities {
|
||||||
@ -58,15 +65,37 @@ object MentionUtilities {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = SpannableString(text)
|
val result = SpannableString(text)
|
||||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
|
||||||
val color = if (isOutgoingMessage) {
|
var mentionTextColour: Int? = null
|
||||||
ResourcesCompat.getColor(context.resources, if (isLightMode) R.color.white else R.color.black, context.theme)
|
// In dark themes..
|
||||||
} else {
|
if (ThemeUtil.isDarkTheme(context)) {
|
||||||
context.getAccentColor()
|
// ..we use the standard outgoing message colour for outgoing messages..
|
||||||
|
if (isOutgoingMessage) {
|
||||||
|
val mentionTextColourAttributeId = getMessageTextColourAttr(true)
|
||||||
|
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
|
||||||
|
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
|
||||||
}
|
}
|
||||||
|
else // ..but we use the accent colour for incoming messages (i.e., someone mentioning us)..
|
||||||
|
{
|
||||||
|
mentionTextColour = context.getAccentColor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // ..while in light themes we always just use the incoming or outgoing message text colour for mentions.
|
||||||
|
{
|
||||||
|
val mentionTextColourAttributeId = getMessageTextColourAttr(isOutgoingMessage)
|
||||||
|
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
|
||||||
|
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
|
||||||
|
}
|
||||||
|
|
||||||
for (mention in mentions) {
|
for (mention in mentions) {
|
||||||
result.setSpan(ForegroundColorSpan(color), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
result.setSpan(ForegroundColorSpan(mentionTextColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
|
||||||
|
// If we're using a light theme then we change the background colour of the mention to be the accent colour
|
||||||
|
if (ThemeUtil.isLightTheme(context)) {
|
||||||
|
val backgroundColour = context.getAccentColor();
|
||||||
|
result.setSpan(BackgroundColorSpan(backgroundColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import androidx.annotation.NonNull;
|
|||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.WindowDebouncer;
|
import org.session.libsession.utilities.WindowDebouncer;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
@ -77,11 +78,11 @@ public abstract class Database {
|
|||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setNotifyConverationListeners(Cursor cursor, long threadId) {
|
protected void setNotifyConversationListeners(Cursor cursor, long threadId) {
|
||||||
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(threadId));
|
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(threadId));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setNotifyConverationListListeners(Cursor cursor) {
|
protected void setNotifyConversationListListeners(Cursor cursor) {
|
||||||
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI);
|
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ import android.database.Cursor
|
|||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.messages.ExpirationDatabaseMetadata
|
import org.session.libsession.messaging.messages.ExpirationDatabaseMetadata
|
||||||
import org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX
|
import org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX
|
||||||
import org.session.libsession.utilities.GroupUtil.OPEN_GROUP_INBOX_PREFIX
|
import org.session.libsession.utilities.GroupUtil.COMMUNITY_INBOX_PREFIX
|
||||||
import org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX
|
import org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
|
|
||||||
class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||||
@ -38,8 +38,8 @@ class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHel
|
|||||||
INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID}
|
INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID}
|
||||||
FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME}
|
FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME}
|
||||||
WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$CLOSED_GROUP_PREFIX%'
|
WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$CLOSED_GROUP_PREFIX%'
|
||||||
AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_PREFIX%'
|
AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$COMMUNITY_PREFIX%'
|
||||||
AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_INBOX_PREFIX%'
|
AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$COMMUNITY_INBOX_PREFIX%'
|
||||||
AND EXISTS (SELECT ${RecipientDatabase.EXPIRE_MESSAGES} FROM ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} = ${RecipientDatabase.TABLE_NAME}.${RecipientDatabase.ADDRESS} AND ${RecipientDatabase.EXPIRE_MESSAGES} > 0)
|
AND EXISTS (SELECT ${RecipientDatabase.EXPIRE_MESSAGES} FROM ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} = ${RecipientDatabase.TABLE_NAME}.${RecipientDatabase.ADDRESS} AND ${RecipientDatabase.EXPIRE_MESSAGES} > 0)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import android.content.ContentValues
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
|
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
|
||||||
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
|
|
||||||
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
||||||
@ -72,7 +73,12 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
"${Companion.messageID} = ? AND $messageType = ?",
|
"${Companion.messageID} = ? AND $messageType = ?",
|
||||||
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
|
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
|
||||||
cursor.getInt(serverID).toLong()
|
cursor.getInt(serverID).toLong()
|
||||||
} ?: return
|
}
|
||||||
|
|
||||||
|
if (serverID == null) {
|
||||||
|
Log.w(this::class.simpleName, "Could not get server ID to delete message with ID: $messageID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
database.beginTransaction()
|
database.beginTransaction()
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ public class MediaDatabase extends Database {
|
|||||||
public Cursor getGalleryMediaForThread(long threadId) {
|
public Cursor getGalleryMediaForThread(long threadId) {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""});
|
Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""});
|
||||||
setNotifyConverationListeners(cursor, threadId);
|
setNotifyConversationListeners(cursor, threadId);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ public class MediaDatabase extends Database {
|
|||||||
public Cursor getDocumentMediaForThread(long threadId) {
|
public Cursor getDocumentMediaForThread(long threadId) {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""});
|
Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""});
|
||||||
setNotifyConverationListeners(cursor, threadId);
|
setNotifyConversationListeners(cursor, threadId);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@ package org.thoughtcrime.securesms.database
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.provider.ContactsContract.CommonDataKinds.BaseTypes
|
|
||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
import com.google.android.mms.pdu_alt.PduHeaders
|
import com.google.android.mms.pdu_alt.PduHeaders
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@ -214,7 +214,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
|
|
||||||
fun getMessage(messageId: Long): Cursor {
|
fun getMessage(messageId: Long): Cursor {
|
||||||
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
|
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
|
||||||
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId))
|
setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId))
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,6 +630,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
if (retrieved.isExpirationUpdate) deleteExpirationTimerMessages(threadId, true.takeUnless { retrieved.isGroup })
|
if (retrieved.isExpirationUpdate) deleteExpirationTimerMessages(threadId, true.takeUnless { retrieved.isGroup })
|
||||||
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
|
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
|
||||||
if (messageId == -1L) {
|
if (messageId == -1L) {
|
||||||
|
Log.w(TAG, "insertSecureDecryptedMessageOutbox believes the MmsDatabase insertion failed.")
|
||||||
return Optional.absent()
|
return Optional.absent()
|
||||||
}
|
}
|
||||||
markAsSent(messageId, true)
|
markAsSent(messageId, true)
|
||||||
@ -859,8 +860,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
*/
|
*/
|
||||||
private fun deleteMessages(messageIds: Array<String?>) {
|
private fun deleteMessages(messageIds: Array<String?>) {
|
||||||
if (messageIds.isEmpty()) {
|
if (messageIds.isEmpty()) {
|
||||||
|
Log.w(TAG, "No message Ids provided to MmsDatabase.deleteMessages - aborting delete operation!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't need thread IDs
|
// don't need thread IDs
|
||||||
val queryBuilder = StringBuilder()
|
val queryBuilder = StringBuilder()
|
||||||
for (i in messageIds.indices) {
|
for (i in messageIds.indices) {
|
||||||
@ -883,6 +886,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
notifyStickerPackListeners()
|
notifyStickerPackListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Caution: The bool returned from `deleteMessage` is NOT "Was the message successfully deleted?"
|
||||||
|
// - it is "Was the thread deleted because removing that message resulted in an empty thread"!
|
||||||
override fun deleteMessage(messageId: Long): Boolean {
|
override fun deleteMessage(messageId: Long): Boolean {
|
||||||
val threadId = getThreadIdForMessage(messageId)
|
val threadId = getThreadIdForMessage(messageId)
|
||||||
val attachmentDatabase = get(context).attachmentDatabase()
|
val attachmentDatabase = get(context).attachmentDatabase()
|
||||||
@ -899,14 +904,15 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
|
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
|
||||||
val attachmentDatabase = get(context).attachmentDatabase()
|
val argsArray = messageIds.map { "?" }
|
||||||
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
val argValues = messageIds.map { it.toString() }.toTypedArray()
|
||||||
|
|
||||||
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
|
val db = databaseHelper.writableDatabase
|
||||||
groupReceiptDatabase.deleteRowsForMessages(messageIds)
|
db.delete(
|
||||||
|
TABLE_NAME,
|
||||||
val database = databaseHelper.writableDatabase
|
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
|
||||||
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
|
argValues
|
||||||
|
)
|
||||||
|
|
||||||
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
||||||
notifyConversationListeners(threadId)
|
notifyConversationListeners(threadId)
|
||||||
@ -1089,8 +1095,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
}
|
}
|
||||||
val whereString = where.substring(0, where.length - 4)
|
val whereString = where.substring(0, where.length - 4)
|
||||||
try {
|
try {
|
||||||
cursor =
|
cursor = db!!.query(TABLE_NAME, arrayOf<String?>(ID), whereString, null, null, null, null)
|
||||||
db!!.query(TABLE_NAME, arrayOf<String?>(ID), whereString, null, null, null, null)
|
|
||||||
val toDeleteStringMessageIds = mutableListOf<String>()
|
val toDeleteStringMessageIds = mutableListOf<String>()
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
toDeleteStringMessageIds += cursor.getLong(0).toString()
|
toDeleteStringMessageIds += cursor.getLong(0).toString()
|
||||||
|
@ -30,6 +30,7 @@ import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
@ -115,6 +116,53 @@ public class MmsSmsDatabase extends Database {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable MessageRecord getSentMessageFor(long timestamp, String serializedAuthor) {
|
||||||
|
// Early exit if the author is not us
|
||||||
|
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||||
|
if (!isOwnNumber) {
|
||||||
|
Log.i(TAG, "Asked to find sent messages but provided author is not us - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
|
||||||
|
MmsSmsDatabase.Reader reader = readerFor(cursor);
|
||||||
|
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
if (messageRecord.isOutgoing())
|
||||||
|
{
|
||||||
|
return messageRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Could not find any message sent from us at provided timestamp - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageRecord getLastSentMessageRecordFromSender(long threadId, String serializedAuthor) {
|
||||||
|
// Early exit if the author is not us
|
||||||
|
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||||
|
if (!isOwnNumber) {
|
||||||
|
Log.i(TAG, "Asked to find last sent message but provided author is not us - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
|
// Try everything with resources so that they auto-close on end of scope
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
|
||||||
|
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
if (messageRecord.isOutgoing()) { return messageRecord; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Could not find last sent message from us in given thread - returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
|
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
|
||||||
return getMessageFor(timestamp, author.serialize());
|
return getMessageFor(timestamp, author.serialize());
|
||||||
}
|
}
|
||||||
@ -183,7 +231,7 @@ public class MmsSmsDatabase extends Database {
|
|||||||
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
|
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
|
||||||
|
|
||||||
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
|
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
|
||||||
setNotifyConverationListeners(cursor, threadId);
|
setNotifyConversationListeners(cursor, threadId);
|
||||||
|
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
@ -209,6 +257,82 @@ public class MmsSmsDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Builds up and returns a list of all all the messages sent by this user in the given thread.
|
||||||
|
// Used to do a pass through our local database to remove records when a user has "Ban & Delete"
|
||||||
|
// called on them in a Community.
|
||||||
|
public Set<MessageRecord> getAllMessageRecordsFromSenderInThread(long threadId, String serializedAuthor) {
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
|
||||||
|
Set<MessageRecord> identifiedMessages = new HashSet<MessageRecord>();
|
||||||
|
|
||||||
|
// Try everything with resources so that they auto-close on end of scope
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
|
||||||
|
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
identifiedMessages.add(messageRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return identifiedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version of the above `getAllMessageRecordsFromSenderInThread` method that returns the message
|
||||||
|
// Ids rather than the set of MessageRecords - currently unused by potentially useful in the future.
|
||||||
|
public Set<Long> getAllMessageIdsFromSenderInThread(long threadId, String serializedAuthor) {
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
|
||||||
|
|
||||||
|
Set<Long> identifiedMessages = new HashSet<Long>();
|
||||||
|
|
||||||
|
// Try everything with resources so that they auto-close on end of scope
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
|
||||||
|
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
identifiedMessages.add(messageRecord.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return identifiedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastSentMessageFromSender(long threadId, String serializedAuthor) {
|
||||||
|
|
||||||
|
// Early exit
|
||||||
|
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||||
|
if (!isOwnNumber) {
|
||||||
|
Log.i(TAG, "Asked to find last sent message but sender isn't us - returning null.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
|
// Try everything with resources so that they auto-close on end of scope
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
|
||||||
|
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||||
|
MessageRecord messageRecord;
|
||||||
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
|
if (messageRecord.isOutgoing()) { return messageRecord.id; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Could not find last sent message from us - returning -1.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastMessageTimestamp(long threadId) {
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getUnread() {
|
public Cursor getUnread() {
|
||||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
|
||||||
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
|
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX;
|
import static org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -123,18 +123,18 @@ public class RecipientDatabase extends Database {
|
|||||||
public static String getUpdateApprovedCommand() {
|
public static String getUpdateApprovedCommand() {
|
||||||
return "UPDATE "+ TABLE_NAME + " " +
|
return "UPDATE "+ TABLE_NAME + " " +
|
||||||
"SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " +
|
"SET " + APPROVED + " = 1, " + APPROVED_ME + " = 1 " +
|
||||||
"WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'";
|
"WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUpdateResetApprovedCommand() {
|
public static String getUpdateResetApprovedCommand() {
|
||||||
return "UPDATE "+ TABLE_NAME + " " +
|
return "UPDATE "+ TABLE_NAME + " " +
|
||||||
"SET " + APPROVED + " = 0, " + APPROVED_ME + " = 0 " +
|
"SET " + APPROVED + " = 0, " + APPROVED_ME + " = 0 " +
|
||||||
"WHERE " + ADDRESS + " NOT LIKE '" + OPEN_GROUP_PREFIX + "%'";
|
"WHERE " + ADDRESS + " NOT LIKE '" + COMMUNITY_PREFIX + "%'";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUpdateApprovedSelectConversations() {
|
public static String getUpdateApprovedSelectConversations() {
|
||||||
return "UPDATE "+ TABLE_NAME + " SET "+APPROVED+" = 1, "+APPROVED_ME+" = 1 "+
|
return "UPDATE "+ TABLE_NAME + " SET "+APPROVED+" = 1, "+APPROVED_ME+" = 1 "+
|
||||||
"WHERE "+ADDRESS+ " NOT LIKE '"+OPEN_GROUP_PREFIX+"%' " +
|
"WHERE "+ADDRESS+ " NOT LIKE '"+ COMMUNITY_PREFIX +"%' " +
|
||||||
"AND ("+ADDRESS+" IN (SELECT "+ThreadDatabase.TABLE_NAME+"."+ThreadDatabase.ADDRESS+" FROM "+ThreadDatabase.TABLE_NAME+" WHERE ("+ThreadDatabase.MESSAGE_COUNT+" != 0) "+
|
"AND ("+ADDRESS+" IN (SELECT "+ThreadDatabase.TABLE_NAME+"."+ThreadDatabase.ADDRESS+" FROM "+ThreadDatabase.TABLE_NAME+" WHERE ("+ThreadDatabase.MESSAGE_COUNT+" != 0) "+
|
||||||
"OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))";
|
"OR "+ADDRESS+" IN (SELECT "+GroupDatabase.TABLE_NAME+"."+GroupDatabase.ADMINS+" FROM "+GroupDatabase.TABLE_NAME+")))";
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import com.annimon.stream.Stream;
|
|||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -115,11 +116,9 @@ public class SearchDatabase extends Database {
|
|||||||
public Cursor queryMessages(@NonNull String query) {
|
public Cursor queryMessages(@NonNull String query) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String prefixQuery = adjustQuery(query);
|
String prefixQuery = adjustQuery(query);
|
||||||
|
|
||||||
int queryLimit = Math.min(query.length()*50,500);
|
int queryLimit = Math.min(query.length()*50,500);
|
||||||
|
|
||||||
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) });
|
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) });
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +127,7 @@ public class SearchDatabase extends Database {
|
|||||||
String prefixQuery = adjustQuery(query);
|
String prefixQuery = adjustQuery(query);
|
||||||
|
|
||||||
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) });
|
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) });
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
return cursor;
|
return cursor;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,15 +22,11 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.session.libsession.messaging.calls.CallMessageType;
|
import org.session.libsession.messaging.calls.CallMessageType;
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
||||||
@ -51,7 +47,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@ -621,17 +616,20 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
public Cursor getMessageCursor(long messageId) {
|
public Cursor getMessageCursor(long messageId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null);
|
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null);
|
||||||
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId));
|
setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId));
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Caution: The bool returned from `deleteMessage` is NOT "Was the message successfully deleted?"
|
||||||
|
// - it is "Was the thread deleted because removing that message resulted in an empty thread"!
|
||||||
@Override
|
@Override
|
||||||
public boolean deleteMessage(long messageId) {
|
public boolean deleteMessage(long messageId) {
|
||||||
Log.i("MessageDatabase", "Deleting: " + messageId);
|
Log.i("MessageDatabase", "Deleting: " + messageId);
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
long threadId = getThreadIdForMessage(messageId);
|
long threadId = getThreadIdForMessage(messageId);
|
||||||
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
||||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
notifyConversationListeners(threadId);
|
||||||
|
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, false);
|
||||||
return threadDeleted;
|
return threadDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,9 +643,6 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
argValues[i] = (messageIds[i] + "");
|
argValues[i] = (messageIds[i] + "");
|
||||||
}
|
}
|
||||||
|
|
||||||
String combinedMessageIdArgss = StringUtils.join(messageIds, ',');
|
|
||||||
String combinedMessageIds = StringUtils.join(messageIds, ',');
|
|
||||||
Log.i("MessageDatabase", "Deleting: " + combinedMessageIds);
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.delete(
|
db.delete(
|
||||||
TABLE_NAME,
|
TABLE_NAME,
|
||||||
@ -701,12 +696,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package */void deleteThread(long threadId) {
|
void deleteMessagesInThreadBeforeDate(long threadId, long date) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
|
||||||
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package*/void deleteMessagesInThreadBeforeDate(long threadId, long date) {
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
String where = THREAD_ID + " = ? AND (CASE " + TYPE;
|
String where = THREAD_ID + " = ? AND (CASE " + TYPE;
|
||||||
|
|
||||||
@ -719,7 +709,12 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
|
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package*/ void deleteThreads(Set<Long> threadIds) {
|
void deleteThread(long threadId) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteThreads(Set<Long> threadIds) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
String where = "";
|
String where = "";
|
||||||
|
|
||||||
@ -727,23 +722,23 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
where += THREAD_ID + " = '" + threadId + "' OR ";
|
where += THREAD_ID + " = '" + threadId + "' OR ";
|
||||||
}
|
}
|
||||||
|
|
||||||
where = where.substring(0, where.length() - 4);
|
where = where.substring(0, where.length() - 4); // Remove the final: "' OR "
|
||||||
|
|
||||||
db.delete(TABLE_NAME, where, null);
|
db.delete(TABLE_NAME, where, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package */ void deleteAllThreads() {
|
void deleteAllThreads() {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.delete(TABLE_NAME, null, null);
|
db.delete(TABLE_NAME, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package*/ SQLiteDatabase beginTransaction() {
|
SQLiteDatabase beginTransaction() {
|
||||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
database.beginTransaction();
|
database.beginTransaction();
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package*/ void endTransaction(SQLiteDatabase database) {
|
void endTransaction(SQLiteDatabase database) {
|
||||||
database.setTransactionSuccessful();
|
database.setTransactionSuccessful();
|
||||||
database.endTransaction();
|
database.endTransaction();
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority
|
|||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import kotlin.time.Duration.Companion.days
|
|
||||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
||||||
|
|
||||||
private const val TAG = "Storage"
|
private const val TAG = "Storage"
|
||||||
@ -100,7 +99,7 @@ private const val TAG = "Storage"
|
|||||||
open class Storage(
|
open class Storage(
|
||||||
context: Context,
|
context: Context,
|
||||||
helper: SQLCipherOpenHelper,
|
helper: SQLCipherOpenHelper,
|
||||||
private val configFactory: ConfigFactory
|
val configFactory: ConfigFactory
|
||||||
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
|
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
|
||||||
|
|
||||||
override fun threadCreated(address: Address, threadId: Long) {
|
override fun threadCreated(address: Address, threadId: Long) {
|
||||||
@ -121,7 +120,7 @@ open class Storage(
|
|||||||
)
|
)
|
||||||
volatile.set(newVolatileParams)
|
volatile.set(newVolatileParams)
|
||||||
}
|
}
|
||||||
} else if (address.isOpenGroup) {
|
} else if (address.isCommunity) {
|
||||||
// these should be added on the group join / group info fetch
|
// these should be added on the group join / group info fetch
|
||||||
Log.w("Loki", "Thread created called for open group address, not adding any extra information")
|
Log.w("Loki", "Thread created called for open group address, not adding any extra information")
|
||||||
}
|
}
|
||||||
@ -152,7 +151,7 @@ open class Storage(
|
|||||||
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||||
volatile.eraseLegacyClosedGroup(sessionId)
|
volatile.eraseLegacyClosedGroup(sessionId)
|
||||||
groups.eraseLegacyGroup(sessionId)
|
groups.eraseLegacyGroup(sessionId)
|
||||||
} else if (address.isOpenGroup) {
|
} else if (address.isCommunity) {
|
||||||
// these should be removed in the group leave / handling new configs
|
// these should be removed in the group leave / handling new configs
|
||||||
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
|
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
|
||||||
}
|
}
|
||||||
@ -182,7 +181,7 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getUserProfile(): Profile {
|
override fun getUserProfile(): Profile {
|
||||||
val displayName = TextSecurePreferences.getProfileName(context)!!
|
val displayName = TextSecurePreferences.getProfileName(context)
|
||||||
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||||
val profilePictureUrl = TextSecurePreferences.getProfilePictureURL(context)
|
val profilePictureUrl = TextSecurePreferences.getProfilePictureURL(context)
|
||||||
return Profile(displayName, profileKey, profilePictureUrl)
|
return Profile(displayName, profileKey, profilePictureUrl)
|
||||||
@ -257,7 +256,7 @@ open class Storage(
|
|||||||
// recipient closed group
|
// recipient closed group
|
||||||
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
|
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
|
||||||
// recipient is open group
|
// recipient is open group
|
||||||
recipient.isOpenGroupRecipient -> {
|
recipient.isCommunityRecipient -> {
|
||||||
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
|
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
|
||||||
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
|
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
|
||||||
config.getOrConstructCommunity(base, room, pubKey)
|
config.getOrConstructCommunity(base, room, pubKey)
|
||||||
@ -327,7 +326,7 @@ open class Storage(
|
|||||||
setRecipientApprovedMe(targetRecipient, true)
|
setRecipientApprovedMe(targetRecipient, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) {
|
if (message.threadID == null && !targetRecipient.isCommunityRecipient) {
|
||||||
// open group recipients should explicitly create threads
|
// open group recipients should explicitly create threads
|
||||||
message.threadID = getOrCreateThreadIdFor(targetAddress)
|
message.threadID = getOrCreateThreadIdFor(targetAddress)
|
||||||
}
|
}
|
||||||
@ -767,13 +766,36 @@ open class Storage(
|
|||||||
|
|
||||||
override fun markAsSent(timestamp: Long, author: String) {
|
override fun markAsSent(timestamp: Long, author: String) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getSentMessageFor(timestamp, author)
|
||||||
|
if (messageRecord == null) {
|
||||||
|
Log.w(TAG, "Failed to retrieve local message record in Storage.markAsSent - aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (messageRecord.isMms) {
|
if (messageRecord.isMms) {
|
||||||
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
DatabaseComponent.get(context).mmsDatabase().markAsSent(messageRecord.getId(), true)
|
||||||
mmsDatabase.markAsSent(messageRecord.getId(), true)
|
|
||||||
} else {
|
} else {
|
||||||
val smsDatabase = DatabaseComponent.get(context).smsDatabase()
|
DatabaseComponent.get(context).smsDatabase().markAsSent(messageRecord.getId(), true)
|
||||||
smsDatabase.markAsSent(messageRecord.getId(), true)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method that marks a message as sent in Communities (only!) - where the server modifies the
|
||||||
|
// message timestamp and as such we cannot use that to identify the local message.
|
||||||
|
override fun markAsSentToCommunity(threadId: Long, messageID: Long) {
|
||||||
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
|
val message = database.getLastSentMessageRecordFromSender(threadId, TextSecurePreferences.getLocalNumber(context))
|
||||||
|
|
||||||
|
// Ensure we can find the local message..
|
||||||
|
if (message == null) {
|
||||||
|
Log.w(TAG, "Could not find local message in Storage.markAsSentToCommunity - aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..and mark as sent if found.
|
||||||
|
if (message.isMms) {
|
||||||
|
DatabaseComponent.get(context).mmsDatabase().markAsSent(message.getId(), true)
|
||||||
|
} else {
|
||||||
|
DatabaseComponent.get(context).smsDatabase().markAsSent(message.getId(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -808,7 +830,11 @@ open class Storage(
|
|||||||
|
|
||||||
override fun markUnidentified(timestamp: Long, author: String) {
|
override fun markUnidentified(timestamp: Long, author: String) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getMessageFor(timestamp, author)
|
||||||
|
if (messageRecord == null) {
|
||||||
|
Log.w(TAG, "Could not identify message with timestamp: $timestamp from author: $author")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (messageRecord.isMms) {
|
if (messageRecord.isMms) {
|
||||||
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
||||||
mmsDatabase.markUnidentified(messageRecord.getId(), true)
|
mmsDatabase.markUnidentified(messageRecord.getId(), true)
|
||||||
@ -818,6 +844,26 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method that marks a message as unidentified in Communities (only!) - where the server
|
||||||
|
// modifies the message timestamp and as such we cannot use that to identify the local message.
|
||||||
|
override fun markUnidentifiedInCommunity(threadId: Long, messageId: Long) {
|
||||||
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
|
val message = database.getLastSentMessageRecordFromSender(threadId, TextSecurePreferences.getLocalNumber(context))
|
||||||
|
|
||||||
|
// Check to ensure the message exists
|
||||||
|
if (message == null) {
|
||||||
|
Log.w(TAG, "Could not find local message in Storage.markUnidentifiedInCommunity - aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark it as unidentified if we found the message successfully
|
||||||
|
if (message.isMms) {
|
||||||
|
DatabaseComponent.get(context).mmsDatabase().markUnidentified(message.getId(), true)
|
||||||
|
} else {
|
||||||
|
DatabaseComponent.get(context).smsDatabase().markUnidentified(message.getId(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) {
|
override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||||
@ -971,7 +1017,10 @@ open class Storage(
|
|||||||
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, 0, true, null, listOf(), listOf())
|
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, 0, true, null, listOf(), listOf())
|
||||||
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
|
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
|
||||||
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
|
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) {
|
||||||
|
Log.w(TAG, "Bailing from insertOutgoingInfoMessage because we believe the message has already been sent!")
|
||||||
|
return
|
||||||
|
}
|
||||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
|
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
|
||||||
mmsDB.markAsSent(infoMessageID, true)
|
mmsDB.markAsSent(infoMessageID, true)
|
||||||
}
|
}
|
||||||
@ -1289,7 +1338,7 @@ open class Storage(
|
|||||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||||
)
|
)
|
||||||
groups.set(newGroupInfo)
|
groups.set(newGroupInfo)
|
||||||
} else if (threadRecipient.isOpenGroupRecipient) {
|
} else if (threadRecipient.isCommunityRecipient) {
|
||||||
val openGroup = getOpenGroup(threadID) ?: return
|
val openGroup = getOpenGroup(threadID) ?: return
|
||||||
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
||||||
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
||||||
@ -1322,31 +1371,31 @@ open class Storage(
|
|||||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||||
val groupDB = DatabaseComponent.get(context).groupDatabase()
|
val groupDB = DatabaseComponent.get(context).groupDatabase()
|
||||||
threadDB.deleteConversation(threadID)
|
threadDB.deleteConversation(threadID)
|
||||||
val recipient = getRecipientForThread(threadID) ?: return
|
|
||||||
when {
|
val recipient = getRecipientForThread(threadID)
|
||||||
recipient.isContactRecipient -> {
|
if (recipient == null) {
|
||||||
if (recipient.isLocalNumber) return
|
Log.w(TAG, "Got null recipient when deleting conversation - aborting.");
|
||||||
val contacts = configFactory.contacts ?: return
|
return
|
||||||
contacts.upsertContact(recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
|
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
|
||||||
}
|
}
|
||||||
recipient.isClosedGroupRecipient -> {
|
|
||||||
// TODO: handle closed group
|
// There is nothing further we need to do if this is a 1-on-1 conversation, and it's not
|
||||||
|
// possible to delete communities in this manner so bail.
|
||||||
|
if (recipient.isContactRecipient || recipient.isCommunityRecipient) return
|
||||||
|
|
||||||
|
// If we get here then this is a closed group conversation (i.e., recipient.isClosedGroupRecipient)
|
||||||
val volatile = configFactory.convoVolatile ?: return
|
val volatile = configFactory.convoVolatile ?: return
|
||||||
val groups = configFactory.userGroups ?: return
|
val groups = configFactory.userGroups ?: return
|
||||||
val groupID = recipient.address.toGroupString()
|
val groupID = recipient.address.toGroupString()
|
||||||
val closedGroup = getGroup(groupID)
|
val closedGroup = getGroup(groupID)
|
||||||
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
|
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
|
||||||
if (closedGroup != null) {
|
if (closedGroup != null) {
|
||||||
groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it)
|
groupDB.delete(groupID)
|
||||||
volatile.eraseLegacyClosedGroup(groupPublicKey)
|
volatile.eraseLegacyClosedGroup(groupPublicKey)
|
||||||
groups.eraseLegacyGroup(groupPublicKey)
|
groups.eraseLegacyGroup(groupPublicKey)
|
||||||
} else {
|
} else {
|
||||||
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
|
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
|
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
|
||||||
return PartAuthority.getAttachmentDataUri(attachmentId)
|
return PartAuthority.getAttachmentDataUri(attachmentId)
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
import static org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX;
|
import static org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX;
|
||||||
import static org.session.libsession.utilities.GroupUtil.OPEN_GROUP_PREFIX;
|
import static org.session.libsession.utilities.GroupUtil.COMMUNITY_PREFIX;
|
||||||
import static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID;
|
import static org.thoughtcrime.securesms.database.GroupDatabase.GROUP_ID;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@ -26,14 +26,10 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.MergeCursor;
|
import android.database.MergeCursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.snode.SnodeAPI;
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
@ -61,7 +57,6 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -83,7 +78,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public static final String TABLE_NAME = "thread";
|
public static final String TABLE_NAME = "thread";
|
||||||
public static final String ID = "_id";
|
public static final String ID = "_id";
|
||||||
public static final String DATE = "date";
|
public static final String THREAD_CREATION_DATE = "date";
|
||||||
public static final String MESSAGE_COUNT = "message_count";
|
public static final String MESSAGE_COUNT = "message_count";
|
||||||
public static final String ADDRESS = "recipient_ids";
|
public static final String ADDRESS = "recipient_ids";
|
||||||
public static final String SNIPPET = "snippet";
|
public static final String SNIPPET = "snippet";
|
||||||
@ -91,7 +86,7 @@ public class ThreadDatabase extends Database {
|
|||||||
public static final String READ = "read";
|
public static final String READ = "read";
|
||||||
public static final String UNREAD_COUNT = "unread_count";
|
public static final String UNREAD_COUNT = "unread_count";
|
||||||
public static final String UNREAD_MENTION_COUNT = "unread_mention_count";
|
public static final String UNREAD_MENTION_COUNT = "unread_mention_count";
|
||||||
public static final String TYPE = "type";
|
public static final String DISTRIBUTION_TYPE = "type"; // See: DistributionTypes.kt
|
||||||
private static final String ERROR = "error";
|
private static final String ERROR = "error";
|
||||||
public static final String SNIPPET_TYPE = "snippet_type";
|
public static final String SNIPPET_TYPE = "snippet_type";
|
||||||
public static final String SNIPPET_URI = "snippet_uri";
|
public static final String SNIPPET_URI = "snippet_uri";
|
||||||
@ -105,23 +100,23 @@ public class ThreadDatabase extends Database {
|
|||||||
public static final String IS_PINNED = "is_pinned";
|
public static final String IS_PINNED = "is_pinned";
|
||||||
|
|
||||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
|
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
|
||||||
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
|
ID + " INTEGER PRIMARY KEY, " + THREAD_CREATION_DATE + " INTEGER DEFAULT 0, " +
|
||||||
MESSAGE_COUNT + " INTEGER DEFAULT 0, " + ADDRESS + " TEXT, " + SNIPPET + " TEXT, " +
|
MESSAGE_COUNT + " INTEGER DEFAULT 0, " + ADDRESS + " TEXT, " + SNIPPET + " TEXT, " +
|
||||||
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
|
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
|
||||||
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
|
DISTRIBUTION_TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
|
||||||
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
|
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
|
||||||
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
|
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
|
||||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||||
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
|
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
|
||||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
||||||
|
|
||||||
public static final String[] CREATE_INDEXS = {
|
public static final String[] CREATE_INDEXES = {
|
||||||
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + ADDRESS + ");",
|
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + ADDRESS + ");",
|
||||||
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
|
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String[] THREAD_PROJECTION = {
|
private static final String[] THREAD_PROJECTION = {
|
||||||
ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, UNREAD_MENTION_COUNT, TYPE, ERROR, SNIPPET_TYPE,
|
ID, THREAD_CREATION_DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, UNREAD_MENTION_COUNT, DISTRIBUTION_TYPE, ERROR, SNIPPET_TYPE,
|
||||||
SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED
|
SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -158,11 +153,10 @@ public class ThreadDatabase extends Database {
|
|||||||
ContentValues contentValues = new ContentValues(4);
|
ContentValues contentValues = new ContentValues(4);
|
||||||
long date = SnodeAPI.getNowWithOffset();
|
long date = SnodeAPI.getNowWithOffset();
|
||||||
|
|
||||||
contentValues.put(DATE, date - date % 1000);
|
contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
|
||||||
contentValues.put(ADDRESS, address.serialize());
|
contentValues.put(ADDRESS, address.serialize());
|
||||||
|
|
||||||
if (group)
|
if (group) contentValues.put(DISTRIBUTION_TYPE, distributionType);
|
||||||
contentValues.put(TYPE, distributionType);
|
|
||||||
|
|
||||||
contentValues.put(MESSAGE_COUNT, 0);
|
contentValues.put(MESSAGE_COUNT, 0);
|
||||||
|
|
||||||
@ -175,7 +169,7 @@ public class ThreadDatabase extends Database {
|
|||||||
long expiresIn, int readReceiptCount)
|
long expiresIn, int readReceiptCount)
|
||||||
{
|
{
|
||||||
ContentValues contentValues = new ContentValues(7);
|
ContentValues contentValues = new ContentValues(7);
|
||||||
contentValues.put(DATE, date - date % 1000);
|
contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
|
||||||
contentValues.put(MESSAGE_COUNT, count);
|
contentValues.put(MESSAGE_COUNT, count);
|
||||||
if (!body.isEmpty()) {
|
if (!body.isEmpty()) {
|
||||||
contentValues.put(SNIPPET, body);
|
contentValues.put(SNIPPET, body);
|
||||||
@ -187,9 +181,7 @@ public class ThreadDatabase extends Database {
|
|||||||
contentValues.put(READ_RECEIPT_COUNT, readReceiptCount);
|
contentValues.put(READ_RECEIPT_COUNT, readReceiptCount);
|
||||||
contentValues.put(EXPIRES_IN, expiresIn);
|
contentValues.put(EXPIRES_IN, expiresIn);
|
||||||
|
|
||||||
if (unarchive) {
|
if (unarchive) { contentValues.put(ARCHIVED, 0); }
|
||||||
contentValues.put(ARCHIVED, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
|
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
|
||||||
@ -199,7 +191,7 @@ public class ThreadDatabase extends Database {
|
|||||||
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) {
|
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) {
|
||||||
ContentValues contentValues = new ContentValues(4);
|
ContentValues contentValues = new ContentValues(4);
|
||||||
|
|
||||||
contentValues.put(DATE, date - date % 1000);
|
contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
|
||||||
if (!snippet.isEmpty()) {
|
if (!snippet.isEmpty()) {
|
||||||
contentValues.put(SNIPPET, snippet);
|
contentValues.put(SNIPPET, snippet);
|
||||||
}
|
}
|
||||||
@ -230,9 +222,7 @@ public class ThreadDatabase extends Database {
|
|||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
String where = "";
|
String where = "";
|
||||||
|
|
||||||
for (long threadId : threadIds) {
|
for (long threadId : threadIds) { where += ID + " = '" + threadId + "' OR "; }
|
||||||
where += ID + " = '" + threadId + "' OR ";
|
|
||||||
}
|
|
||||||
|
|
||||||
where = where.substring(0, where.length() - 4);
|
where = where.substring(0, where.length() - 4);
|
||||||
|
|
||||||
@ -358,7 +348,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public void setDistributionType(long threadId, int distributionType) {
|
public void setDistributionType(long threadId, int distributionType) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
contentValues.put(TYPE, distributionType);
|
contentValues.put(DISTRIBUTION_TYPE, distributionType);
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||||
@ -367,7 +357,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public void setDate(long threadId, long date) {
|
public void setDate(long threadId, long date) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
contentValues.put(DATE, date);
|
contentValues.put(THREAD_CREATION_DATE, date);
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
|
int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
|
||||||
if (updated > 0) notifyConversationListListeners();
|
if (updated > 0) notifyConversationListListeners();
|
||||||
@ -375,11 +365,11 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public int getDistributionType(long threadId) {
|
public int getDistributionType(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = db.query(TABLE_NAME, new String[]{TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
Cursor cursor = db.query(TABLE_NAME, new String[]{DISTRIBUTION_TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
return cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
|
return cursor.getInt(cursor.getColumnIndexOrThrow(DISTRIBUTION_TYPE));
|
||||||
}
|
}
|
||||||
|
|
||||||
return DistributionTypes.DEFAULT;
|
return DistributionTypes.DEFAULT;
|
||||||
@ -427,7 +417,7 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0);
|
Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0);
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,7 +459,7 @@ public class ThreadDatabase extends Database {
|
|||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String where = "SELECT " + DATE + " FROM " + TABLE_NAME +
|
String where = "SELECT " + THREAD_CREATION_DATE + " FROM " + TABLE_NAME +
|
||||||
" LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME +
|
" LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME +
|
||||||
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
|
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
|
||||||
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
||||||
@ -477,7 +467,7 @@ public class ThreadDatabase extends Database {
|
|||||||
" WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " +
|
" WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " +
|
||||||
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
|
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
|
||||||
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " +
|
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " +
|
||||||
GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL ORDER BY " + DATE + " DESC LIMIT 1";
|
GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL ORDER BY " + THREAD_CREATION_DATE + " DESC LIMIT 1";
|
||||||
cursor = db.rawQuery(where, null);
|
cursor = db.rawQuery(where, null);
|
||||||
|
|
||||||
if (cursor != null && cursor.moveToFirst())
|
if (cursor != null && cursor.moveToFirst())
|
||||||
@ -491,7 +481,7 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getConversationList() {
|
public Cursor getConversationList() {
|
||||||
String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
|
String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " +
|
||||||
"AND " + ARCHIVED + " = 0 ";
|
"AND " + ARCHIVED + " = 0 ";
|
||||||
return getConversationList(where);
|
return getConversationList(where);
|
||||||
}
|
}
|
||||||
@ -502,7 +492,7 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getApprovedConversationList() {
|
public Cursor getApprovedConversationList() {
|
||||||
String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
|
String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + COMMUNITY_PREFIX + "%') " +
|
||||||
"AND " + ARCHIVED + " = 0 ";
|
"AND " + ARCHIVED + " = 0 ";
|
||||||
return getConversationList(where);
|
return getConversationList(where);
|
||||||
}
|
}
|
||||||
@ -515,18 +505,12 @@ public class ThreadDatabase extends Database {
|
|||||||
return getConversationList(where);
|
return getConversationList(where);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getArchivedConversationList() {
|
|
||||||
String where = "(" + MESSAGE_COUNT + " != 0 OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
|
|
||||||
"AND " + ARCHIVED + " = 1 ";
|
|
||||||
return getConversationList(where);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cursor getConversationList(String where) {
|
private Cursor getConversationList(String where) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
String query = createQuery(where, 0);
|
String query = createQuery(where, 0);
|
||||||
Cursor cursor = db.rawQuery(query, null);
|
Cursor cursor = db.rawQuery(query, null);
|
||||||
|
|
||||||
setNotifyConverationListListeners(cursor);
|
setNotifyConversationListListeners(cursor);
|
||||||
|
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
@ -547,7 +531,7 @@ public class ThreadDatabase extends Database {
|
|||||||
// edge case where we set the last seen time for a conversation before it loads messages (joining community for example)
|
// edge case where we set the last seen time for a conversation before it loads messages (joining community for example)
|
||||||
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||||
Recipient forThreadId = getRecipientForThreadId(threadId);
|
Recipient forThreadId = getRecipientForThreadId(threadId);
|
||||||
if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isOpenGroupRecipient()) return false;
|
if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isCommunityRecipient()) return false;
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
@ -601,7 +585,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public Long getLastUpdated(long threadId) {
|
public Long getLastUpdated(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = db.query(TABLE_NAME, new String[]{DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
Cursor cursor = db.query(TABLE_NAME, new String[]{THREAD_CREATION_DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
@ -742,7 +726,7 @@ public class ThreadDatabase extends Database {
|
|||||||
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||||
long count = mmsSmsDatabase.getConversationCount(threadId);
|
long count = mmsSmsDatabase.getConversationCount(threadId);
|
||||||
|
|
||||||
boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && deleteThreadOnEmpty(threadId);
|
boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && possibleToDeleteThreadOnEmpty(threadId);
|
||||||
|
|
||||||
if (count == 0 && shouldDeleteEmptyThread) {
|
if (count == 0 && shouldDeleteEmptyThread) {
|
||||||
deleteThread(threadId);
|
deleteThread(threadId);
|
||||||
@ -750,10 +734,7 @@ public class ThreadDatabase extends Database {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
MmsSmsDatabase.Reader reader = null;
|
try (MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId))) {
|
||||||
|
|
||||||
try {
|
|
||||||
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
|
|
||||||
MessageRecord record = null;
|
MessageRecord record = null;
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
record = reader.getNext();
|
record = reader.getNext();
|
||||||
@ -771,11 +752,10 @@ public class ThreadDatabase extends Database {
|
|||||||
deleteThread(threadId);
|
deleteThread(threadId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// todo: add empty snippet that clears existing data
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (reader != null)
|
|
||||||
reader.close();
|
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
}
|
}
|
||||||
@ -820,9 +800,9 @@ public class ThreadDatabase extends Database {
|
|||||||
return setLastSeen(threadId, lastSeenTime);
|
return setLastSeen(threadId, lastSeenTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean deleteThreadOnEmpty(long threadId) {
|
private boolean possibleToDeleteThreadOnEmpty(long threadId) {
|
||||||
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
||||||
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
|
return threadRecipient != null && !threadRecipient.isCommunityRecipient();
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||||
@ -865,7 +845,7 @@ public class ThreadDatabase extends Database {
|
|||||||
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
||||||
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
|
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
|
||||||
" WHERE " + where +
|
" WHERE " + where +
|
||||||
" ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + DATE + " DESC";
|
" ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + THREAD_CREATION_DATE + " DESC";
|
||||||
|
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
query += " LIMIT " + limit;
|
query += " LIMIT " + limit;
|
||||||
@ -910,7 +890,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public ThreadRecord getCurrent() {
|
public ThreadRecord getCurrent() {
|
||||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
|
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
|
||||||
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE));
|
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DISTRIBUTION_TYPE));
|
||||||
Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS)));
|
Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS)));
|
||||||
|
|
||||||
Optional<RecipientSettings> settings;
|
Optional<RecipientSettings> settings;
|
||||||
@ -926,7 +906,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
Recipient recipient = Recipient.from(context, address, settings, groupRecord, true);
|
Recipient recipient = Recipient.from(context, address, settings, groupRecord, true);
|
||||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
|
String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
|
||||||
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
|
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.THREAD_CREATION_DATE));
|
||||||
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
||||||
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
||||||
int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT));
|
int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT));
|
||||||
@ -944,7 +924,17 @@ public class ThreadDatabase extends Database {
|
|||||||
readReceiptCount = 0;
|
readReceiptCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ThreadRecord(body, snippetUri, recipient, date, count,
|
MessageRecord lastMessage = null;
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||||
|
long messageTimestamp = mmsSmsDatabase.getLastMessageTimestamp(threadId);
|
||||||
|
if (messageTimestamp > 0) {
|
||||||
|
lastMessage = mmsSmsDatabase.getMessageForTimestamp(messageTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ThreadRecord(body, snippetUri, lastMessage, recipient, date, count,
|
||||||
unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type,
|
unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type,
|
||||||
distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
|
distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
|
||||||
}
|
}
|
||||||
|
@ -357,7 +357,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, AttachmentDatabase.CREATE_INDEXS);
|
executeStatements(db, AttachmentDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, ThreadDatabase.CREATE_INDEXS);
|
executeStatements(db, ThreadDatabase.CREATE_INDEXES);
|
||||||
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
||||||
|
@ -22,7 +22,10 @@ import android.text.SpannableString;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +51,9 @@ public abstract class DisplayRecord {
|
|||||||
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
|
||||||
long type, int readReceiptCount)
|
long type, int readReceiptCount)
|
||||||
{
|
{
|
||||||
|
// TODO: This gets hit very, very often and it likely shouldn't - place a Log.d statement in it to see.
|
||||||
|
//Log.d("[ACL]", "Creating a display record with delivery status of: " + deliveryStatus);
|
||||||
|
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
this.dateSent = dateSent;
|
this.dateSent = dateSent;
|
||||||
@ -72,13 +78,11 @@ public abstract class DisplayRecord {
|
|||||||
public int getReadReceiptCount() { return readReceiptCount; }
|
public int getReadReceiptCount() { return readReceiptCount; }
|
||||||
|
|
||||||
public boolean isDelivered() {
|
public boolean isDelivered() {
|
||||||
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE
|
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE &&
|
||||||
&& deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSent() {
|
public boolean isSent() { return MmsSmsColumns.Types.isSentType(type); }
|
||||||
return !isFailed() && !isPending();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSyncing() {
|
public boolean isSyncing() {
|
||||||
return MmsSmsColumns.Types.isSyncingType(type);
|
return MmsSmsColumns.Types.isSyncingType(type);
|
||||||
@ -99,9 +103,10 @@ public abstract class DisplayRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPending() {
|
public boolean isPending() {
|
||||||
return MmsSmsColumns.Types.isPendingMessageType(type)
|
boolean isPending = MmsSmsColumns.Types.isPendingMessageType(type) &&
|
||||||
&& !MmsSmsColumns.Types.isIdentityVerified(type)
|
!MmsSmsColumns.Types.isIdentityVerified(type) &&
|
||||||
&& !MmsSmsColumns.Types.isIdentityDefault(type);
|
!MmsSmsColumns.Types.isIdentityDefault(type);
|
||||||
|
return isPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRead() { return readReceiptCount > 0; }
|
public boolean isRead() { return readReceiptCount > 0; }
|
||||||
@ -109,6 +114,11 @@ public abstract class DisplayRecord {
|
|||||||
public boolean isOutgoing() {
|
public boolean isOutgoing() {
|
||||||
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isIncoming() {
|
||||||
|
return !MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isGroupUpdateMessage() {
|
public boolean isGroupUpdateMessage() {
|
||||||
return SmsDatabase.Types.isGroupUpdateMessage(type);
|
return SmsDatabase.Types.isGroupUpdateMessage(type);
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import org.session.libsession.messaging.utilities.UpdateMessageData;
|
|||||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||||
import org.session.libsession.utilities.NetworkFailure;
|
import org.session.libsession.utilities.NetworkFailure;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -120,7 +121,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
||||||
} else if (isExpirationTimerUpdate()) {
|
} else if (isExpirationTimerUpdate()) {
|
||||||
int seconds = (int) (getExpiresIn() / 1000);
|
int seconds = (int) (getExpiresIn() / 1000);
|
||||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getRecipient(), getIndividualRecipient().getAddress().serialize(), isOutgoing(), getTimestamp(), expireStarted));
|
boolean isGroup = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(getThreadId()).isGroupRecipient();
|
||||||
|
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, isGroup, getIndividualRecipient().getAddress().serialize(), isOutgoing(), getTimestamp(), expireStarted));
|
||||||
} else if (isDataExtractionNotification()) {
|
} else if (isDataExtractionNotification()) {
|
||||||
if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
|
if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
|
||||||
else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
|
else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
|
||||||
|
@ -43,6 +43,7 @@ import network.loki.messenger.R;
|
|||||||
public class ThreadRecord extends DisplayRecord {
|
public class ThreadRecord extends DisplayRecord {
|
||||||
|
|
||||||
private @Nullable final Uri snippetUri;
|
private @Nullable final Uri snippetUri;
|
||||||
|
public @Nullable final MessageRecord lastMessage;
|
||||||
private final long count;
|
private final long count;
|
||||||
private final int unreadCount;
|
private final int unreadCount;
|
||||||
private final int unreadMentionCount;
|
private final int unreadMentionCount;
|
||||||
@ -54,13 +55,14 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
private final int initialRecipientHash;
|
private final int initialRecipientHash;
|
||||||
|
|
||||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||||
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||||
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
||||||
long snippetType, int distributionType, boolean archived, long expiresIn,
|
long snippetType, int distributionType, boolean archived, long expiresIn,
|
||||||
long lastSeen, int readReceiptCount, boolean pinned)
|
long lastSeen, int readReceiptCount, boolean pinned)
|
||||||
{
|
{
|
||||||
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
||||||
this.snippetUri = snippetUri;
|
this.snippetUri = snippetUri;
|
||||||
|
this.lastMessage = lastMessage;
|
||||||
this.count = count;
|
this.count = count;
|
||||||
this.unreadCount = unreadCount;
|
this.unreadCount = unreadCount;
|
||||||
this.unreadMentionCount = unreadMentionCount;
|
this.unreadMentionCount = unreadMentionCount;
|
||||||
|
@ -78,7 +78,7 @@ class CreateGroupFragment : Fragment() {
|
|||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
if (name.length >= 30) {
|
if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) {
|
||||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
|
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
val selectedMembers = adapter.selectedMembers
|
val selectedMembers = adapter.selectedMembers
|
||||||
|
@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
|
||||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -75,7 +74,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
|
|||||||
}
|
}
|
||||||
binding.copyConversationId.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE
|
binding.copyConversationId.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE
|
||||||
binding.copyConversationId.setOnClickListener(this)
|
binding.copyConversationId.setOnClickListener(this)
|
||||||
binding.copyCommunityUrl.visibility = if (recipient.isOpenGroupRecipient) View.VISIBLE else View.GONE
|
binding.copyCommunityUrl.visibility = if (recipient.isCommunityRecipient) View.VISIBLE else View.GONE
|
||||||
binding.copyCommunityUrl.setOnClickListener(this)
|
binding.copyCommunityUrl.setOnClickListener(this)
|
||||||
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||||
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||||
|
@ -4,6 +4,8 @@ import android.content.Context
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.TextUtils
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -89,7 +91,7 @@ class ConversationView : LinearLayout {
|
|||||||
|| (configFactory.convoVolatile?.getConversationUnread(thread) == true)
|
|| (configFactory.convoVolatile?.getConversationUnread(thread) == true)
|
||||||
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||||
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
|
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
|
||||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
val senderDisplayName = getTitle(thread.recipient)
|
||||||
?: thread.recipient.address.toString()
|
?: thread.recipient.address.toString()
|
||||||
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
||||||
binding.timestampTextView.text = thread.date.takeIf { it != 0L }?.let { DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), it) }
|
binding.timestampTextView.text = thread.date.takeIf { it != 0L }?.let { DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), it) }
|
||||||
@ -101,9 +103,7 @@ class ConversationView : LinearLayout {
|
|||||||
R.drawable.ic_notifications_mentions
|
R.drawable.ic_notifications_mentions
|
||||||
}
|
}
|
||||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||||
val rawSnippet = thread.getDisplayBody(context)
|
binding.snippetTextView.text = highlightMentions(thread.getSnippet(), thread.threadId, context)
|
||||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
|
||||||
binding.snippetTextView.text = snippet
|
|
||||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
@ -131,12 +131,21 @@ class ConversationView : LinearLayout {
|
|||||||
binding.profilePictureView.recycle()
|
binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
private fun getTitle(recipient: Recipient): String? = when {
|
||||||
return if (recipient.isLocalNumber) {
|
recipient.isLocalNumber -> context.getString(R.string.note_to_self)
|
||||||
context.getString(R.string.note_to_self)
|
else -> recipient.toShortString() // Internally uses the Contact API
|
||||||
} else {
|
|
||||||
recipient.toShortString() // Internally uses the Contact API
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ThreadRecord.getSnippet(): CharSequence =
|
||||||
|
concatSnippet(getSnippetPrefix(), getDisplayBody(context))
|
||||||
|
|
||||||
|
private fun concatSnippet(prefix: CharSequence?, body: CharSequence): CharSequence =
|
||||||
|
prefix?.let { TextUtils.concat(it, ": ", body) } ?: body
|
||||||
|
|
||||||
|
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
||||||
|
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
||||||
|
lastMessage?.isOutgoing == true -> resources.getString(R.string.MessageRecord_you)
|
||||||
|
else -> lastMessage?.individualRecipient?.toShortString()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -293,7 +293,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
val newData = contactResults + messageResults
|
val newData = contactResults + messageResults
|
||||||
|
|
||||||
globalSearchAdapter.setNewData(result.query, newData)
|
globalSearchAdapter.setNewData(result.query, newData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -496,7 +495,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
else if (thread.recipient.isOpenGroupRecipient) {
|
else if (thread.recipient.isCommunityRecipient) {
|
||||||
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
|
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
|
||||||
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.databinding.ActivityPathBinding
|
import network.loki.messenger.databinding.ActivityPathBinding
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.Snode
|
import org.session.libsignal.utilities.Snode
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
|
@ -25,7 +25,6 @@ import org.session.libsession.utilities.recipients.Recipient
|
|||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@ -34,6 +33,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
|
|
||||||
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
|
private lateinit var binding: FragmentUserDetailsBottomSheetBinding
|
||||||
|
|
||||||
|
private var previousContactNickname: String = ""
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
const val ARGUMENT_PUBLIC_KEY = "publicKey"
|
||||||
const val ARGUMENT_THREAD_ID = "threadId"
|
const val ARGUMENT_THREAD_ID = "threadId"
|
||||||
@ -89,10 +91,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
&& !threadRecipient.isOpenGroupInboxRecipient
|
&& !threadRecipient.isOpenGroupInboxRecipient
|
||||||
&& !threadRecipient.isOpenGroupOutboxRecipient
|
&& !threadRecipient.isOpenGroupOutboxRecipient
|
||||||
|
|
||||||
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
publicKeyTextView.isVisible = !threadRecipient.isCommunityRecipient
|
||||||
&& !threadRecipient.isOpenGroupInboxRecipient
|
&& !threadRecipient.isOpenGroupInboxRecipient
|
||||||
&& !threadRecipient.isOpenGroupOutboxRecipient
|
&& !threadRecipient.isOpenGroupOutboxRecipient
|
||||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
|
messageButton.isVisible = !threadRecipient.isCommunityRecipient || IdPrefix.fromValue(publicKey)?.isBlinded() == true
|
||||||
publicKeyTextView.text = publicKey
|
publicKeyTextView.text = publicKey
|
||||||
publicKeyTextView.setOnLongClickListener {
|
publicKeyTextView.setOnLongClickListener {
|
||||||
val clipboard =
|
val clipboard =
|
||||||
@ -130,9 +132,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
nameTextViewContainer.visibility = View.VISIBLE
|
nameTextViewContainer.visibility = View.VISIBLE
|
||||||
nameEditTextContainer.visibility = View.INVISIBLE
|
nameEditTextContainer.visibility = View.INVISIBLE
|
||||||
var newNickName: String? = null
|
var newNickName: String? = null
|
||||||
if (nicknameEditText.text.isNotEmpty()) {
|
if (nicknameEditText.text.isNotEmpty() && nicknameEditText.text.trim().length != 0) {
|
||||||
newNickName = nicknameEditText.text.toString()
|
newNickName = nicknameEditText.text.toString()
|
||||||
}
|
}
|
||||||
|
else { newNickName = previousContactNickname }
|
||||||
val publicKey = recipient.address.serialize()
|
val publicKey = recipient.address.serialize()
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
|
val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
|
||||||
@ -145,6 +148,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
fun showSoftKeyboard() {
|
fun showSoftKeyboard() {
|
||||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
imm?.showSoftInput(binding.nicknameEditText, 0)
|
imm?.showSoftInput(binding.nicknameEditText, 0)
|
||||||
|
|
||||||
|
// Keep track of the original nickname to re-use if an empty / blank nickname is entered
|
||||||
|
previousContactNickname = binding.nameTextView.text.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideSoftKeyboard() {
|
fun hideSoftKeyboard() {
|
||||||
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.home.search
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.text.InputFilter.LengthFilter
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
@ -34,6 +36,7 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
|
|||||||
binding.searchInput.onFocusChangeListener = this
|
binding.searchInput.onFocusChangeListener = this
|
||||||
binding.searchInput.addTextChangedListener(this)
|
binding.searchInput.addTextChangedListener(this)
|
||||||
binding.searchInput.setOnEditorActionListener(this)
|
binding.searchInput.setOnEditorActionListener(this)
|
||||||
|
binding.searchInput.setFilters( arrayOf<InputFilter>(LengthFilter(100)) ) // 100 char search limit
|
||||||
binding.searchCancel.setOnClickListener(this)
|
binding.searchCancel.setOnClickListener(this)
|
||||||
binding.searchClear.setOnClickListener(this)
|
binding.searchClear.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,7 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
|
|||||||
|
|
||||||
private val executor = viewModelScope + SupervisorJob()
|
private val executor = viewModelScope + SupervisorJob()
|
||||||
|
|
||||||
private val _result: MutableStateFlow<GlobalSearchResult> =
|
private val _result: MutableStateFlow<GlobalSearchResult> = MutableStateFlow(GlobalSearchResult.EMPTY)
|
||||||
MutableStateFlow(GlobalSearchResult.EMPTY)
|
|
||||||
|
|
||||||
val result: StateFlow<GlobalSearchResult> = _result
|
val result: StateFlow<GlobalSearchResult> = _result
|
||||||
|
|
||||||
@ -41,13 +40,14 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
|
|||||||
_queryText
|
_queryText
|
||||||
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
.mapLatest { query ->
|
.mapLatest { query ->
|
||||||
if (query.trim().length < 2) {
|
// Early exit on empty search query
|
||||||
|
if (query.trim().isEmpty()) {
|
||||||
SearchResult.EMPTY
|
SearchResult.EMPTY
|
||||||
} else {
|
} else {
|
||||||
// user input delay here in case we get a new query within a few hundred ms
|
// User input delay in case we get a new query within a few hundred ms this
|
||||||
// this coroutine will be cancelled and expensive query will not be run if typing quickly
|
// coroutine will be cancelled and the expensive query will not be run.
|
||||||
// first query of 2 characters will be instant however
|
|
||||||
delay(300)
|
delay(300)
|
||||||
|
|
||||||
val settableFuture = SettableFuture<SearchResult>()
|
val settableFuture = SettableFuture<SearchResult>()
|
||||||
searchRepository.query(query.toString(), settableFuture::set)
|
searchRepository.query(query.toString(), settableFuture::set)
|
||||||
try {
|
try {
|
||||||
@ -64,6 +64,4 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
|
|||||||
}
|
}
|
||||||
.launchIn(executor)
|
.launchIn(executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -349,11 +349,17 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
builder.setThread(notifications.get(0).getRecipient());
|
builder.setThread(notifications.get(0).getRecipient());
|
||||||
builder.setMessageCount(notificationState.getMessageCount());
|
builder.setMessageCount(notificationState.getMessageCount());
|
||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(notifications.get(0).getThreadId(),context);
|
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(notifications.get(0).getThreadId(),context);
|
||||||
|
|
||||||
|
// TODO: Removing highlighting mentions in the notification because this context is the libsession one which
|
||||||
|
// TODO: doesn't have access to the `R.attr.message_sent_text_color` and `R.attr.message_received_text_color`
|
||||||
|
// TODO: attributes to perform the colour lookup. Also, it makes little sense to highlight the mentions using
|
||||||
|
// TODO: the app theme as it may result in insufficient contrast with the notification background which will
|
||||||
|
// TODO: be using the SYSTEM theme.
|
||||||
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
|
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
|
||||||
MentionUtilities.highlightMentions(text == null ? "" : text,
|
//MentionUtilities.highlightMentions(text == null ? "" : text, notifications.get(0).getThreadId(), context), // Removing hightlighting mentions -ACL
|
||||||
notifications.get(0).getThreadId(),
|
text == null ? "" : text,
|
||||||
context),
|
|
||||||
notifications.get(0).getSlideDeck());
|
notifications.get(0).getSlideDeck());
|
||||||
|
|
||||||
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
|
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
|
||||||
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
|
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
|
||||||
builder.setOnlyAlertOnce(!signal);
|
builder.setOnlyAlertOnce(!signal);
|
||||||
|
@ -61,11 +61,15 @@ class MarkReadReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
|
val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
|
|
||||||
|
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||||
|
|
||||||
// start disappear after read messages except TimerUpdates in groups.
|
// start disappear after read messages except TimerUpdates in groups.
|
||||||
markedReadMessages
|
markedReadMessages
|
||||||
.filter { it.expiryType == ExpiryType.AFTER_READ }
|
.filter { it.expiryType == ExpiryType.AFTER_READ }
|
||||||
.map { it.syncMessageId }
|
.map { it.syncMessageId }
|
||||||
.filter { mmsSmsDatabase.getMessageForTimestamp(it.timetamp)?.run { isExpirationTimerUpdate && recipient.isClosedGroupRecipient } == false }
|
.filter { mmsSmsDatabase.getMessageForTimestamp(it.timetamp)?.run {
|
||||||
|
isExpirationTimerUpdate && threadDb.getRecipientForThreadId(threadId)?.isGroupRecipient == true } == false
|
||||||
|
}
|
||||||
.forEach { messageExpirationManager.startDisappearAfterRead(it.timetamp, it.address.serialize()) }
|
.forEach { messageExpirationManager.startDisappearAfterRead(it.timetamp, it.address.serialize()) }
|
||||||
|
|
||||||
hashToDisappearAfterReadMessage(context, markedReadMessages)?.let {
|
hashToDisappearAfterReadMessage(context, markedReadMessages)?.let {
|
||||||
|
@ -53,7 +53,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
|
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
|
||||||
String displayName = recipient.toShortString();
|
String displayName = recipient.toShortString();
|
||||||
if (threadRecipient.isGroupRecipient()) {
|
if (threadRecipient.isGroupRecipient()) {
|
||||||
displayName = getGroupDisplayName(recipient, threadRecipient.isOpenGroupRecipient());
|
displayName = getGroupDisplayName(recipient, threadRecipient.isCommunityRecipient());
|
||||||
}
|
}
|
||||||
if (privacy.isDisplayContact()) {
|
if (privacy.isDisplayContact()) {
|
||||||
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName));
|
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName));
|
||||||
@ -79,7 +79,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) {
|
public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) {
|
||||||
String displayName = sender.toShortString();
|
String displayName = sender.toShortString();
|
||||||
if (threadRecipient.isGroupRecipient()) {
|
if (threadRecipient.isGroupRecipient()) {
|
||||||
displayName = getGroupDisplayName(sender, threadRecipient.isOpenGroupRecipient());
|
displayName = getGroupDisplayName(sender, threadRecipient.isCommunityRecipient());
|
||||||
}
|
}
|
||||||
if (privacy.isDisplayMessage()) {
|
if (privacy.isDisplayMessage()) {
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
|
@ -125,7 +125,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
||||||
|
|
||||||
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
||||||
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
|
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
|
||||||
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
||||||
|
|
||||||
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
||||||
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
|
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isCommunityRecipient());
|
||||||
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.onboarding
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.text.style.ClickableSpan
|
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
|
||||||
import org.session.libsession.snode.SnodeModule
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
|
||||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.utilities.KeyHelper
|
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
|
||||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
|
||||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
|
||||||
import org.thoughtcrime.securesms.util.push
|
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var configFactory: ConfigFactory
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
|
||||||
internal val database: LokiAPIDatabaseProtocol
|
|
||||||
get() = SnodeModule.shared.storage
|
|
||||||
// region Lifecycle
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setUpActionBarSessionLogo()
|
|
||||||
TextSecurePreferences.apply {
|
|
||||||
setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true)
|
|
||||||
setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false)
|
|
||||||
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
|
||||||
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
binding.mnemonicEditText.imeOptions = binding.mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
|
||||||
binding.restoreButton.setOnClickListener { restore() }
|
|
||||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/terms-of-service/")
|
|
||||||
}
|
|
||||||
}, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/privacy-policy/")
|
|
||||||
}
|
|
||||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
binding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
binding.termsTextView.text = termsExplanation
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Interaction
|
|
||||||
private fun restore() {
|
|
||||||
val mnemonic = binding.mnemonicEditText.text.toString()
|
|
||||||
try {
|
|
||||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
|
||||||
// which can result in an invalid database state
|
|
||||||
database.clearAllLastMessageHashes()
|
|
||||||
database.clearReceivedMessageHashValues()
|
|
||||||
|
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
|
||||||
}
|
|
||||||
val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic)
|
|
||||||
val seed = Hex.fromStringCondensed(hexEncodedSeed)
|
|
||||||
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
|
|
||||||
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
|
||||||
KeyPairUtilities.store(this, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
|
|
||||||
configFactory.keyPairChanged()
|
|
||||||
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
|
|
||||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
|
||||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
|
||||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
|
||||||
val intent = Intent(this, DisplayNameActivity::class.java)
|
|
||||||
push(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val message = if (e is MnemonicCodec.DecodingError) e.description else MnemonicCodec.DecodingError.Generic.description
|
|
||||||
return Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openURL(url: String) {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -5,9 +5,14 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
|
|
||||||
@ -67,6 +72,19 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateExportButtonAndProgressBarUI(exportJobRunning: Boolean) {
|
||||||
|
this.activity?.runOnUiThread(Runnable {
|
||||||
|
// Change export logs button text
|
||||||
|
val exportLogsButton = this.activity?.findViewById(R.id.export_logs_button) as TextView?
|
||||||
|
if (exportLogsButton == null) { Log.w("Loki", "Could not find export logs button view.") }
|
||||||
|
exportLogsButton?.text = if (exportJobRunning) getString(R.string.cancel) else getString(R.string.activity_help_settings__export_logs)
|
||||||
|
|
||||||
|
// Show progress bar
|
||||||
|
val exportProgressBar = this.activity?.findViewById(R.id.export_progress_bar) as ProgressBar?
|
||||||
|
exportProgressBar?.isInvisible = !exportJobRunning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private fun shareLogs() {
|
private fun shareLogs() {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
@ -76,7 +94,7 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() {
|
|||||||
Toast.makeText(requireActivity(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
|
Toast.makeText(requireActivity(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
.onAllGranted {
|
.onAllGranted {
|
||||||
ShareLogsDialog().show(parentFragmentManager,"Share Logs Dialog")
|
ShareLogsDialog(::updateExportButtonAndProgressBarUI).show(parentFragmentManager,"Share Logs Dialog")
|
||||||
}
|
}
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import android.view.ActionMode
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@ -203,6 +204,21 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.displayNameEditText.selectAll()
|
binding.displayNameEditText.selectAll()
|
||||||
binding.displayNameEditText.requestFocus()
|
binding.displayNameEditText.requestFocus()
|
||||||
inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
|
inputMethodManager.showSoftInput(binding.displayNameEditText, 0)
|
||||||
|
|
||||||
|
// Save the updated display name when the user presses enter on the soft keyboard
|
||||||
|
binding.displayNameEditText.setOnEditorActionListener { v, actionId, event ->
|
||||||
|
when (actionId) {
|
||||||
|
// Note: IME_ACTION_DONE is how we've configured the soft keyboard to respond,
|
||||||
|
// while IME_ACTION_UNSPECIFIED is what triggers when we hit enter on a
|
||||||
|
// physical keyboard.
|
||||||
|
EditorInfo.IME_ACTION_DONE, EditorInfo.IME_ACTION_UNSPECIFIED -> {
|
||||||
|
saveDisplayName()
|
||||||
|
displayNameEditActionMode?.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||||
}
|
}
|
||||||
|
@ -11,55 +11,73 @@ import android.os.Bundle
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
import network.loki.messenger.BuildConfig
|
import network.loki.messenger.BuildConfig
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
|
||||||
import org.session.libsignal.utilities.ExternalStorageUtil
|
import org.session.libsignal.utilities.ExternalStorageUtil
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.createSessionDialog
|
import org.thoughtcrime.securesms.createSessionDialog
|
||||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||||
import org.thoughtcrime.securesms.util.StreamUtil
|
import org.thoughtcrime.securesms.util.StreamUtil
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ShareLogsDialog : DialogFragment() {
|
|
||||||
|
|
||||||
|
class ShareLogsDialog(private val updateCallback: (Boolean)->Unit): DialogFragment() {
|
||||||
|
|
||||||
|
private val TAG = "ShareLogsDialog"
|
||||||
private var shareJob: Job? = null
|
private var shareJob: Job? = null
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||||
title(R.string.dialog_share_logs_title)
|
title(R.string.dialog_share_logs_title)
|
||||||
text(R.string.dialog_share_logs_explanation)
|
text(R.string.dialog_share_logs_explanation)
|
||||||
button(R.string.share, dismiss = false) { shareLogs() }
|
button(R.string.share, dismiss = false) { runShareLogsJob() }
|
||||||
cancelButton { dismiss() }
|
cancelButton { updateCallback(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareLogs() {
|
// If the share logs dialog loses focus the job gets cancelled so we'll update the UI state
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
updateCallback(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runShareLogsJob() {
|
||||||
|
// Cancel any existing share job that might already be running to start anew
|
||||||
shareJob?.cancel()
|
shareJob?.cancel()
|
||||||
|
|
||||||
|
updateCallback(true)
|
||||||
|
|
||||||
shareJob = lifecycleScope.launch(Dispatchers.IO) {
|
shareJob = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val persistentLogger = ApplicationContext.getInstance(context).persistentLogger
|
val persistentLogger = ApplicationContext.getInstance(context).persistentLogger
|
||||||
try {
|
try {
|
||||||
|
Log.d(TAG, "Starting share logs job...")
|
||||||
|
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
val outputUri: Uri = ExternalStorageUtil.getDownloadUri()
|
val outputUri: Uri = ExternalStorageUtil.getDownloadUri()
|
||||||
val mediaUri = getExternalFile()
|
val mediaUri = getExternalFile() ?: return@launch
|
||||||
if (mediaUri == null) {
|
|
||||||
// show toast saying media saved
|
|
||||||
dismiss()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val inputStream = persistentLogger.logs.get().byteInputStream()
|
val inputStream = persistentLogger.logs.get().byteInputStream()
|
||||||
val updateValues = ContentValues()
|
val updateValues = ContentValues()
|
||||||
|
|
||||||
|
// Add details into the output or media files as appropriate
|
||||||
if (outputUri.scheme == ContentResolver.SCHEME_FILE) {
|
if (outputUri.scheme == ContentResolver.SCHEME_FILE) {
|
||||||
FileOutputStream(mediaUri.path).use { outputStream ->
|
FileOutputStream(mediaUri.path).use { outputStream ->
|
||||||
StreamUtil.copy(inputStream, outputStream)
|
StreamUtil.copy(inputStream, outputStream)
|
||||||
@ -73,6 +91,7 @@ class ShareLogsDialog : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT > 28) {
|
if (Build.VERSION.SDK_INT > 28) {
|
||||||
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||||
}
|
}
|
||||||
@ -95,13 +114,35 @@ class ShareLogsDialog : DialogFragment() {
|
|||||||
}
|
}
|
||||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
Log.e("Loki", "Error saving logs", e)
|
Log.e("Loki", "Error saving logs", e)
|
||||||
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}.also { shareJob ->
|
||||||
|
shareJob.invokeOnCompletion { handler ->
|
||||||
|
// Note: Don't show Toasts here directly - use `withContext(Main)` or such if req'd
|
||||||
|
handler?.message.let { msg ->
|
||||||
|
if (shareJob.isCancelled) {
|
||||||
|
if (msg.isNullOrBlank()) {
|
||||||
|
Log.w(TAG, "Share logs job was cancelled.")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Share logs job was cancelled. Reason: $msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (shareJob.isCompleted) {
|
||||||
|
Log.d(TAG, "Share logs job completed. Msg: $msg")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.w(TAG, "Share logs job finished while still Active. Msg: $msg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regardless of the job's success it has now completed so update the UI
|
||||||
|
updateCallback(false)
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,5 +199,4 @@ class ShareLogsDialog : DialogFragment() {
|
|||||||
return context.contentResolver.insert(outputUri, contentValues)
|
return context.contentResolver.insert(outputUri, contentValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,11 +1,15 @@
|
|||||||
package org.thoughtcrime.securesms.repository
|
package org.thoughtcrime.securesms.repository
|
||||||
|
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.cash.copper.Query
|
import app.cash.copper.Query
|
||||||
import app.cash.copper.flow.observeQuery
|
import app.cash.copper.flow.observeQuery
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
@ -22,6 +26,7 @@ import org.session.libsession.utilities.Address
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||||
@ -40,9 +45,6 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
interface ConversationRepository {
|
interface ConversationRepository {
|
||||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||||
@ -55,37 +57,19 @@ interface ConversationRepository {
|
|||||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||||
fun setBlocked(recipient: Recipient, blocked: Boolean)
|
fun setBlocked(recipient: Recipient, blocked: Boolean)
|
||||||
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||||
|
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
|
||||||
fun setApproved(recipient: Recipient, isApproved: Boolean)
|
fun setApproved(recipient: Recipient, isApproved: Boolean)
|
||||||
|
suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): ResultOf<Unit>
|
||||||
suspend fun deleteForEveryone(
|
|
||||||
threadId: Long,
|
|
||||||
recipient: Recipient,
|
|
||||||
message: MessageRecord
|
|
||||||
): ResultOf<Unit>
|
|
||||||
|
|
||||||
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
|
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
|
||||||
|
suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<MessageRecord>): ResultOf<Unit>
|
||||||
suspend fun deleteMessageWithoutUnsendRequest(
|
|
||||||
threadId: Long,
|
|
||||||
messages: Set<MessageRecord>
|
|
||||||
): ResultOf<Unit>
|
|
||||||
|
|
||||||
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun deleteThread(threadId: Long): ResultOf<Unit>
|
suspend fun deleteThread(threadId: Long): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
|
suspend fun deleteMessageRequest(thread: ThreadRecord): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit>
|
suspend fun clearAllMessageRequests(block: Boolean): ResultOf<Unit>
|
||||||
|
|
||||||
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): ResultOf<Unit>
|
||||||
|
|
||||||
fun declineMessageRequest(threadId: Long)
|
fun declineMessageRequest(threadId: Long)
|
||||||
|
|
||||||
fun hasReceived(threadId: Long): Boolean
|
fun hasReceived(threadId: Long): Boolean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultConversationRepository @Inject constructor(
|
class DefaultConversationRepository @Inject constructor(
|
||||||
@ -184,6 +168,15 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) {
|
||||||
|
val threadId = messageRecord.threadId
|
||||||
|
val senderId = messageRecord.recipient.address.contactIdentifier()
|
||||||
|
val messageRecordsToRemoveFromLocalStorage = mmsSmsDb.getAllMessageRecordsFromSenderInThread(threadId, senderId)
|
||||||
|
for (message in messageRecordsToRemoveFromLocalStorage) {
|
||||||
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun setApproved(recipient: Recipient, isApproved: Boolean) {
|
override fun setApproved(recipient: Recipient, isApproved: Boolean) {
|
||||||
storage.setRecipientApproved(recipient, isApproved)
|
storage.setRecipientApproved(recipient, isApproved)
|
||||||
}
|
}
|
||||||
@ -196,18 +189,38 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||||
MessageSender.send(unsendRequest, recipient.address)
|
MessageSender.send(unsendRequest, recipient.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
if (openGroup != null) {
|
if (openGroup != null) {
|
||||||
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
val serverId = lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
||||||
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
|
Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..")
|
||||||
continuation.resumeWithException(error)
|
continuation.resumeWithException(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the server ID is null then this message is stuck in limbo (it has likely been
|
||||||
|
// deleted remotely but that deletion did not occur locally) - so we'll delete the
|
||||||
|
// message locally to clean up.
|
||||||
|
if (serverId == null) {
|
||||||
|
Log.w("ConversationRepository","Found community message without a server ID - deleting locally.")
|
||||||
|
|
||||||
|
// Caution: The bool returned from `deleteMessage` is NOT "Was the message
|
||||||
|
// successfully deleted?" - it is "Was the thread itself also deleted because
|
||||||
|
// removing that message resulted in an empty thread?".
|
||||||
|
if (message.isMms) {
|
||||||
|
mmsDb.deleteMessage(message.id)
|
||||||
} else {
|
} else {
|
||||||
|
smsDb.deleteMessage(message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // If this thread is NOT in a Community
|
||||||
|
{
|
||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash ->
|
messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash ->
|
||||||
var publicKey = recipient.address.serialize()
|
var publicKey = recipient.address.serialize()
|
||||||
@ -218,6 +231,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
.success {
|
.success {
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
|
Log.w("ConversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
|
||||||
continuation.resumeWithException(error)
|
continuation.resumeWithException(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,7 +239,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
|
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
|
||||||
if (recipient.isOpenGroupRecipient) return null
|
if (recipient.isCommunityRecipient) return null
|
||||||
messageDataProvider.getServerHashForMessage(message.id, message.isMms) ?: return null
|
messageDataProvider.getServerHashForMessage(message.id, message.isMms) ?: return null
|
||||||
return UnsendRequest(
|
return UnsendRequest(
|
||||||
author = message.takeUnless { it.isOutgoing }?.run { individualRecipient.address.contactIdentifier() } ?: textSecurePreferences.getLocalNumber(),
|
author = message.takeUnless { it.isOutgoing }?.run { individualRecipient.address.contactIdentifier() } ?: textSecurePreferences.getLocalNumber(),
|
||||||
@ -279,8 +293,10 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||||
suspendCoroutine { continuation ->
|
suspendCoroutine { continuation ->
|
||||||
|
// Note: This sessionId could be the blinded Id
|
||||||
val sessionID = recipient.address.toString()
|
val sessionID = recipient.address.toString()
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||||
|
|
||||||
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
@ -306,9 +322,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
while (reader.next != null) {
|
while (reader.next != null) {
|
||||||
deleteMessageRequest(reader.current)
|
deleteMessageRequest(reader.current)
|
||||||
val recipient = reader.current.recipient
|
val recipient = reader.current.recipient
|
||||||
if (block) {
|
if (block) { setBlocked(recipient, true) }
|
||||||
setBlocked(recipient, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ResultOf.Success(Unit)
|
return ResultOf.Success(Unit)
|
||||||
@ -335,9 +349,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
val cursor = mmsSmsDb.getConversation(threadId, true)
|
val cursor = mmsSmsDb.getConversation(threadId, true)
|
||||||
mmsSmsDb.readerFor(cursor).use { reader ->
|
mmsSmsDb.readerFor(cursor).use { reader ->
|
||||||
while (reader.next != null) {
|
while (reader.next != null) {
|
||||||
if (!reader.current.isOutgoing) {
|
if (!reader.current.isOutgoing) { return true }
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -4,12 +4,8 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
import android.database.MergeCursor;
|
import android.database.MergeCursor;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.session.libsession.messaging.contacts.Contact;
|
import org.session.libsession.messaging.contacts.Contact;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.GroupRecord;
|
import org.session.libsession.utilities.GroupRecord;
|
||||||
@ -27,37 +23,25 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
|||||||
import org.thoughtcrime.securesms.search.model.MessageResult;
|
import org.thoughtcrime.securesms.search.model.MessageResult;
|
||||||
import org.thoughtcrime.securesms.search.model.SearchResult;
|
import org.thoughtcrime.securesms.search.model.SearchResult;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
import kotlin.Pair;
|
import kotlin.Pair;
|
||||||
|
|
||||||
/**
|
// Class to manage data retrieval for search
|
||||||
* Manages data retrieval for search.
|
|
||||||
*/
|
|
||||||
public class SearchRepository {
|
public class SearchRepository {
|
||||||
|
|
||||||
private static final String TAG = SearchRepository.class.getSimpleName();
|
private static final String TAG = SearchRepository.class.getSimpleName();
|
||||||
|
|
||||||
private static final Set<Character> BANNED_CHARACTERS = new HashSet<>();
|
private static final Set<Character> BANNED_CHARACTERS = new HashSet<>();
|
||||||
static {
|
static {
|
||||||
// Several ranges of invalid ASCII characters
|
// Construct a list containing several ranges of invalid ASCII characters
|
||||||
for (int i = 33; i <= 47; i++) {
|
// See: https://www.ascii-code.com/
|
||||||
BANNED_CHARACTERS.add((char) i);
|
for (int i = 33; i <= 47; i++) { BANNED_CHARACTERS.add((char) i); } // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /
|
||||||
}
|
for (int i = 58; i <= 64; i++) { BANNED_CHARACTERS.add((char) i); } // :, ;, <, =, >, ?, @
|
||||||
for (int i = 58; i <= 64; i++) {
|
for (int i = 91; i <= 96; i++) { BANNED_CHARACTERS.add((char) i); } // [, \, ], ^, _, `
|
||||||
BANNED_CHARACTERS.add((char) i);
|
for (int i = 123; i <= 126; i++) { BANNED_CHARACTERS.add((char) i); } // {, |, }, ~
|
||||||
}
|
|
||||||
for (int i = 91; i <= 96; i++) {
|
|
||||||
BANNED_CHARACTERS.add((char) i);
|
|
||||||
}
|
|
||||||
for (int i = 123; i <= 126; i++) {
|
|
||||||
BANNED_CHARACTERS.add((char) i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -86,25 +70,25 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
|
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
|
||||||
if (TextUtils.isEmpty(query)) {
|
// If the sanitized search is empty then abort without search
|
||||||
|
String cleanQuery = sanitizeQuery(query).trim();
|
||||||
|
if (cleanQuery.isEmpty()) {
|
||||||
callback.onResult(SearchResult.EMPTY);
|
callback.onResult(SearchResult.EMPTY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
Stopwatch timer = new Stopwatch("FtsQuery");
|
Stopwatch timer = new Stopwatch("FtsQuery");
|
||||||
|
|
||||||
String cleanQuery = sanitizeQuery(query);
|
|
||||||
timer.split("clean");
|
timer.split("clean");
|
||||||
|
|
||||||
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
||||||
timer.split("contacts");
|
timer.split("Contacts");
|
||||||
|
|
||||||
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
|
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
|
||||||
timer.split("conversations");
|
timer.split("Conversations");
|
||||||
|
|
||||||
CursorList<MessageResult> messages = queryMessages(cleanQuery);
|
CursorList<MessageResult> messages = queryMessages(cleanQuery);
|
||||||
timer.split("messages");
|
timer.split("Messages");
|
||||||
|
|
||||||
timer.stop(TAG);
|
timer.stop(TAG);
|
||||||
|
|
||||||
@ -113,22 +97,20 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) {
|
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) {
|
||||||
if (TextUtils.isEmpty(query)) {
|
// If the sanitized search query is empty then abort the search
|
||||||
|
String cleanQuery = sanitizeQuery(query).trim();
|
||||||
|
if (cleanQuery.isEmpty()) {
|
||||||
callback.onResult(CursorList.emptyList());
|
callback.onResult(CursorList.emptyList());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
long startTime = System.currentTimeMillis();
|
CursorList<MessageResult> messages = queryMessages(cleanQuery, threadId);
|
||||||
CursorList<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
|
|
||||||
Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
|
|
||||||
|
|
||||||
callback.onResult(messages);
|
callback.onResult(messages);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
||||||
|
|
||||||
Cursor contacts = contactDatabase.queryContactsByName(query);
|
Cursor contacts = contactDatabase.queryContactsByName(query);
|
||||||
List<Address> contactList = new ArrayList<>();
|
List<Address> contactList = new ArrayList<>();
|
||||||
List<String> contactStrings = new ArrayList<>();
|
List<String> contactStrings = new ArrayList<>();
|
||||||
@ -155,7 +137,6 @@ public class SearchRepository {
|
|||||||
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
|
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
|
||||||
|
|
||||||
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
|
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
|
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
|
||||||
@ -178,9 +159,7 @@ public class SearchRepository {
|
|||||||
membersGroupList.close();
|
membersGroupList.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
|
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
|
||||||
|
|
||||||
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
|
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
|
||||||
: CursorList.emptyList();
|
: CursorList.emptyList();
|
||||||
}
|
}
|
||||||
@ -245,9 +224,7 @@ public class SearchRepository {
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
RecipientModelBuilder(@NonNull Context context) {
|
RecipientModelBuilder(@NonNull Context context) { this.context = context; }
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Recipient build(@NonNull Cursor cursor) {
|
public Recipient build(@NonNull Cursor cursor) {
|
||||||
@ -290,9 +267,7 @@ public class SearchRepository {
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
MessageModelBuilder(@NonNull Context context) {
|
MessageModelBuilder(@NonNull Context context) { this.context = context; }
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MessageResult build(@NonNull Cursor cursor) {
|
public MessageResult build(@NonNull Cursor cursor) {
|
||||||
|
@ -151,8 +151,8 @@ class ExpiringMessageManager(context: Context) : MessageExpirationManagerProtoco
|
|||||||
|
|
||||||
val userPublicKey = getLocalNumber(context)
|
val userPublicKey = getLocalNumber(context)
|
||||||
val senderPublicKey = message.sender
|
val senderPublicKey = message.sender
|
||||||
val sentTimestamp = if (message.sentTimestamp == null) 0 else message.sentTimestamp!!
|
val sentTimestamp = message.sentTimestamp ?: 0
|
||||||
val expireStartedAt = if (expiryMode is AfterSend || message.isSenderSelf) sentTimestamp else 0
|
val expireStartedAt = if ((expiryMode is AfterSend || message.isSenderSelf) && !message.isGroup) sentTimestamp else 0
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (senderPublicKey == null || userPublicKey == senderPublicKey) {
|
if (senderPublicKey == null || userPublicKey == senderPublicKey) {
|
||||||
|
@ -182,9 +182,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) {
|
fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) {
|
||||||
val intent = Intent(ACTION_WANTS_TO_ANSWER)
|
val intent = Intent(ACTION_WANTS_TO_ANSWER).putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer)
|
||||||
.putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer)
|
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,9 +504,9 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAnswerCall(intent: Intent) {
|
private fun handleAnswerCall(intent: Intent) {
|
||||||
val recipient = callManager.recipient ?: return
|
val recipient = callManager.recipient ?: return Log.e(TAG, "No recipient to answer in handleAnswerCall")
|
||||||
val pending = callManager.pendingOffer ?: return
|
val pending = callManager.pendingOffer ?: return Log.e(TAG, "No pending offer in handleAnswerCall")
|
||||||
val callId = callManager.callId ?: return
|
val callId = callManager.callId ?: return Log.e(TAG, "No callId in handleAnswerCall")
|
||||||
val timestamp = callManager.pendingOfferTime
|
val timestamp = callManager.pendingOfferTime
|
||||||
|
|
||||||
if (callManager.currentConnectionState != CallState.RemoteRing) {
|
if (callManager.currentConnectionState != CallState.RemoteRing) {
|
||||||
@ -526,9 +524,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
insertMissedCall(recipient, true)
|
insertMissedCall(recipient, true)
|
||||||
terminate()
|
terminate()
|
||||||
}
|
}
|
||||||
if (didHangup) {
|
if (didHangup) { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callManager.postConnectionEvent(Event.SendAnswer) {
|
callManager.postConnectionEvent(Event.SendAnswer) {
|
||||||
@ -686,7 +682,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
private fun registerPowerButtonReceiver() {
|
private fun registerPowerButtonReceiver() {
|
||||||
if (powerButtonReceiver == null) {
|
if (powerButtonReceiver == null) {
|
||||||
powerButtonReceiver = PowerButtonReceiver()
|
powerButtonReceiver = PowerButtonReceiver()
|
||||||
|
|
||||||
registerReceiver(powerButtonReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
|
registerReceiver(powerButtonReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -719,7 +714,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleCheckTimeout(intent: Intent) {
|
private fun handleCheckTimeout(intent: Intent) {
|
||||||
val callId = callManager.callId ?: return
|
val callId = callManager.callId ?: return
|
||||||
val callState = callManager.currentConnectionState
|
val callState = callManager.currentConnectionState
|
||||||
@ -746,9 +740,9 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) {
|
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) {
|
||||||
// start an intent for the fullscreen
|
// Start an intent for the fullscreen call activity
|
||||||
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java)
|
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java)
|
||||||
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT)
|
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
|
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
|
||||||
startActivity(foregroundIntent)
|
startActivity(foregroundIntent)
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ object ConfigurationMessageUtilities {
|
|||||||
while (current != null) {
|
while (current != null) {
|
||||||
val recipient = current.recipient
|
val recipient = current.recipient
|
||||||
val contact = when {
|
val contact = when {
|
||||||
recipient.isOpenGroupRecipient -> {
|
recipient.isCommunityRecipient -> {
|
||||||
val openGroup = storage.getOpenGroup(current.threadId) ?: continue
|
val openGroup = storage.getOpenGroup(current.threadId) ?: continue
|
||||||
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
|
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
|
||||||
convoConfig.getOrConstructCommunity(base, room, pubKey)
|
convoConfig.getOrConstructCommunity(base, room, pubKey)
|
||||||
@ -279,7 +279,7 @@ object ConfigurationMessageUtilities {
|
|||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val DELETE_INACTIVE_ONE_TO_ONES: String = """
|
val DELETE_INACTIVE_ONE_TO_ONES: String = """
|
||||||
DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.OPEN_GROUP_INBOX_PREFIX}%';
|
DELETE FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.CLOSED_GROUP_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.COMMUNITY_PREFIX}%' AND ${ThreadDatabase.ADDRESS} NOT LIKE '${GroupUtil.COMMUNITY_INBOX_PREFIX}%';
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
}
|
}
|
@ -37,3 +37,8 @@ val RecyclerView.isScrolledToBottom: Boolean
|
|||||||
get() = computeVerticalScrollOffset().coerceAtLeast(0) +
|
get() = computeVerticalScrollOffset().coerceAtLeast(0) +
|
||||||
computeVerticalScrollExtent() +
|
computeVerticalScrollExtent() +
|
||||||
toPx(50, resources) >= computeVerticalScrollRange()
|
toPx(50, resources) >= computeVerticalScrollRange()
|
||||||
|
|
||||||
|
val RecyclerView.isScrolledToWithin30dpOfBottom: Boolean
|
||||||
|
get() = computeVerticalScrollOffset().coerceAtLeast(0) +
|
||||||
|
computeVerticalScrollExtent() +
|
||||||
|
toPx(30, resources) >= computeVerticalScrollRange()
|
@ -14,7 +14,7 @@ fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Bool
|
|||||||
return getOneToOne(recipient.address.serialize())?.unread == true
|
return getOneToOne(recipient.address.serialize())?.unread == true
|
||||||
} else if (recipient.isClosedGroupRecipient) {
|
} else if (recipient.isClosedGroupRecipient) {
|
||||||
return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true
|
return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true
|
||||||
} else if (recipient.isOpenGroupRecipient) {
|
} else if (recipient.isCommunityRecipient) {
|
||||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false
|
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false
|
||||||
return getCommunity(openGroup.server, openGroup.room)?.unread == true
|
return getCommunity(openGroup.server, openGroup.room)?.unread == true
|
||||||
}
|
}
|
||||||
|
@ -37,12 +37,10 @@ public class Stopwatch {
|
|||||||
for (int i = 1; i < splits.size(); i++) {
|
for (int i = 1; i < splits.size(); i++) {
|
||||||
out.append(splits.get(i).label).append(": ");
|
out.append(splits.get(i).label).append(": ");
|
||||||
out.append(splits.get(i).time - splits.get(i - 1).time);
|
out.append(splits.get(i).time - splits.get(i - 1).time);
|
||||||
out.append(" ");
|
out.append("ms ");
|
||||||
}
|
}
|
||||||
|
out.append("total: ").append(splits.get(splits.size() - 1).time - startTime).append("ms.");
|
||||||
out.append("total: ").append(splits.get(splits.size() - 1).time - startTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, out.toString());
|
Log.d(tag, out.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,13 +9,17 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DimenRes
|
import androidx.annotation.DimenRes
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
import androidx.core.graphics.applyCanvas
|
import androidx.core.graphics.applyCanvas
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun View.contains(point: PointF): Boolean {
|
fun View.contains(point: PointF): Boolean {
|
||||||
@ -32,6 +36,24 @@ val View.hitRect: Rect
|
|||||||
@ColorInt
|
@ColorInt
|
||||||
fun Context.getAccentColor() = getColorFromAttr(R.attr.colorAccent)
|
fun Context.getAccentColor() = getColorFromAttr(R.attr.colorAccent)
|
||||||
|
|
||||||
|
// Method to grab the appropriate attribute for a message colour.
|
||||||
|
// Note: This is an attribute, NOT a resource Id - see `getColorResourceIdFromAttr` for that.
|
||||||
|
@AttrRes
|
||||||
|
fun getMessageTextColourAttr(messageIsOutgoing: Boolean): Int {
|
||||||
|
return if (messageIsOutgoing) R.attr.message_sent_text_color else R.attr.message_received_text_color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get an actual R.id.<SOME_COLOUR> resource Id from an attribute such as R.attr.message_sent_text_color etc.
|
||||||
|
@ColorRes
|
||||||
|
fun getColorResourceIdFromAttr(context: Context, attr: Int): Int {
|
||||||
|
val outTypedValue = TypedValue()
|
||||||
|
val successfullyFoundAttribute = context.theme.resolveAttribute(attr, outTypedValue, true)
|
||||||
|
if (successfullyFoundAttribute) { return outTypedValue.resourceId }
|
||||||
|
|
||||||
|
Log.w("ViewUtils", "Could not find colour attribute $attr in theme - using grey as a safe fallback")
|
||||||
|
return R.color.gray50
|
||||||
|
}
|
||||||
|
|
||||||
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
|
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
|
||||||
val startSize = resources.getDimension(startSizeID)
|
val startSize = resources.getDimension(startSizeID)
|
||||||
val endSize = resources.getDimension(endSizeID)
|
val endSize = resources.getDimension(endSizeID)
|
||||||
@ -70,7 +92,6 @@ fun View.hideKeyboard() {
|
|||||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap {
|
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap {
|
||||||
val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth)
|
val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth)
|
||||||
val scale = size.width / measuredWidth.toFloat()
|
val scale = size.width / measuredWidth.toFloat()
|
||||||
|
@ -408,6 +408,10 @@ class CallManager(
|
|||||||
|
|
||||||
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
||||||
localCameraState = newCameraState
|
localCameraState = newCameraState
|
||||||
|
|
||||||
|
// If the camera we've switched to is the front one then mirror it to match what someone
|
||||||
|
// would see when looking in the mirror rather than the left<-->right flipped version.
|
||||||
|
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
|
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
|
||||||
@ -639,7 +643,11 @@ class CallManager(
|
|||||||
peerConnection?.let { connection ->
|
peerConnection?.let { connection ->
|
||||||
connection.flipCamera()
|
connection.flipCamera()
|
||||||
localCameraState = connection.getCameraState()
|
localCameraState = connection.getCameraState()
|
||||||
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
|
|
||||||
|
// Note: We cannot set the mirrored state of the localRenderer here because
|
||||||
|
// localCameraState.activeDirection is still PENDING (not FRONT or BACK) until the flip
|
||||||
|
// completes and we hit Camera.onCameraSwitchDone (followed by PeerConnectionWrapper.onCameraSwitchCompleted
|
||||||
|
// and CallManager.onCameraSwitchCompleted).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,8 +326,6 @@ class PeerConnectionWrapper(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
||||||
// mirror rotation offset
|
|
||||||
rotationVideoSink.mirrored = newCameraState.activeDirection == CameraState.Direction.FRONT
|
|
||||||
cameraEventListener.onCameraSwitchCompleted(newCameraState)
|
cameraEventListener.onCameraSwitchCompleted(newCameraState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class Camera(context: Context,
|
|||||||
Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")
|
Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
activeDirection = PENDING
|
activeDirection = PENDING // Note: The activeDirection will be PENDING until `onCameraSwitchDone`
|
||||||
capturer.switchCamera(this)
|
capturer.switchCamera(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
app/src/main/res/drawable/cross.xml
Normal file
12
app/src/main/res/drawable/cross.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="100dp"
|
||||||
|
android:height="100dp"
|
||||||
|
android:viewportWidth="100"
|
||||||
|
android:viewportHeight="100">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0 L100,100 M0,100 L100,0"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="@android:color/white" />
|
||||||
|
</vector>
|
@ -4,7 +4,6 @@
|
|||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<solid android:color="?android:textColorPrimary"/>
|
<solid android:color="?android:textColorPrimary"/>
|
||||||
<corners android:radius="@dimen/medium_button_corner_radius" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<solid android:color="?android:textColorPrimary"/>
|
<solid android:color="?android:textColorPrimary"/>
|
||||||
<corners android:radius="@dimen/medium_button_corner_radius" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
|
@ -43,6 +43,8 @@
|
|||||||
android:paddingBottom="0dp"
|
android:paddingBottom="0dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:inputType="textCapWords"
|
android:inputType="textCapWords"
|
||||||
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
|
android:maxLines="1"
|
||||||
android:hint="@string/activity_display_name_edit_text_hint" />
|
android:hint="@string/activity_display_name_edit_text_hint" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
|
||||||
android:layout_marginRight="@dimen/very_large_spacing"
|
|
||||||
android:textSize="@dimen/very_large_font_size"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
android:text="@string/activity_restore_title" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
|
||||||
android:layout_marginTop="7dp"
|
|
||||||
android:layout_marginRight="@dimen/very_large_spacing"
|
|
||||||
android:textSize="@dimen/medium_font_size"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
android:text="@string/activity_restore_explanation" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
style="@style/SessionEditText"
|
|
||||||
android:id="@+id/mnemonicEditText"
|
|
||||||
android:contentDescription="@string/AccessibilityId_enter_your_recovery_phrase"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="64dp"
|
|
||||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:layout_marginRight="@dimen/very_large_spacing"
|
|
||||||
android:paddingTop="0dp"
|
|
||||||
android:paddingBottom="0dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:maxLines="3"
|
|
||||||
android:hint="@string/activity_restore_seed_edit_text_hint" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
|
||||||
android:id="@+id/restoreButton"
|
|
||||||
android:contentDescription="@string/AccessibilityId_continue"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/medium_button_height"
|
|
||||||
android:layout_marginLeft="@dimen/massive_spacing"
|
|
||||||
android:layout_marginRight="@dimen/massive_spacing"
|
|
||||||
android:text="@string/continue_2" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/termsTextView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/onboarding_button_bottom_offset"
|
|
||||||
android:layout_marginLeft="@dimen/massive_spacing"
|
|
||||||
android:layout_marginRight="@dimen/massive_spacing"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="?android:textColorTertiary"
|
|
||||||
android:textColorLink="?colorAccent"
|
|
||||||
android:textSize="@dimen/very_small_font_size"
|
|
||||||
android:text="By using this service, you agree to our Terms of Service and Privacy Policy"
|
|
||||||
tools:ignore="HardcodedText" /> <!-- Intentionally not yet translated -->
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -23,6 +23,10 @@
|
|||||||
|
|
||||||
</androidx.appcompat.widget.Toolbar>
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Add this to the below recycler view if you need to debug activity `adjustResize` issues:
|
||||||
|
android:background="@drawable/cross"
|
||||||
|
-->
|
||||||
<org.thoughtcrime.securesms.conversation.v2.ConversationRecyclerView
|
<org.thoughtcrime.securesms.conversation.v2.ConversationRecyclerView
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
android:id="@+id/conversationRecyclerView"
|
android:id="@+id/conversationRecyclerView"
|
||||||
@ -31,6 +35,7 @@
|
|||||||
android:layout_above="@+id/typingIndicatorViewContainer"
|
android:layout_above="@+id/typingIndicatorViewContainer"
|
||||||
android:layout_below="@id/toolbar" />
|
android:layout_below="@id/toolbar" />
|
||||||
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
|
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
android:id="@+id/typingIndicatorViewContainer"
|
android:id="@+id/typingIndicatorViewContainer"
|
||||||
|
@ -43,6 +43,8 @@
|
|||||||
android:paddingBottom="0dp"
|
android:paddingBottom="0dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:inputType="textCapWords"
|
android:inputType="textCapWords"
|
||||||
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
|
android:maxLines="1"
|
||||||
android:hint="@string/activity_display_name_edit_text_hint" />
|
android:hint="@string/activity_display_name_edit_text_hint" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
android:id="@+id/btnCancelGroupNameEdit"
|
android:id="@+id/btnCancelGroupNameEdit"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginLeft="@dimen/medium_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_cancel_name_change"
|
android:contentDescription="@string/AccessibilityId_cancel_name_change"
|
||||||
android:src="@drawable/ic_baseline_clear_24"/>
|
android:src="@drawable/ic_baseline_clear_24"/>
|
||||||
|
|
||||||
@ -49,6 +50,7 @@
|
|||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
|
android:maxLength="@integer/max_group_and_community_name_length_chars"
|
||||||
android:contentDescription="@string/AccessibilityId_group_name"
|
android:contentDescription="@string/AccessibilityId_group_name"
|
||||||
android:hint="@string/activity_edit_closed_group_edit_text_hint" />
|
android:hint="@string/activity_edit_closed_group_edit_text_hint" />
|
||||||
|
|
||||||
@ -57,6 +59,7 @@
|
|||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginRight="@dimen/medium_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_accept_name_change"
|
android:contentDescription="@string/AccessibilityId_accept_name_change"
|
||||||
android:src="@drawable/ic_baseline_done_24"/>
|
android:src="@drawable/ic_baseline_done_24"/>
|
||||||
|
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
|
||||||
android:layout_marginRight="@dimen/very_large_spacing"
|
|
||||||
android:textSize="@dimen/large_font_size"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
android:text="@string/activity_restore_title" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:layout_marginRight="@dimen/very_large_spacing"
|
|
||||||
android:textSize="@dimen/small_font_size"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
android:text="@string/activity_restore_explanation" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/mnemonicEditText"
|
|
||||||
style="@style/SmallSessionEditText"
|
|
||||||
android:contentDescription="@string/AccessibilityId_enter_your_recovery_phrase"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="64dp"
|
|
||||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:layout_marginRight="@dimen/very_large_spacing"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:hint="@string/activity_restore_seed_edit_text_hint"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:maxLines="3"
|
|
||||||
android:paddingTop="0dp"
|
|
||||||
android:paddingBottom="0dp" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
|
||||||
android:contentDescription="@string/AccessibilityId_continue"
|
|
||||||
android:id="@+id/restoreButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/medium_button_height"
|
|
||||||
android:layout_marginLeft="@dimen/massive_spacing"
|
|
||||||
android:layout_marginRight="@dimen/massive_spacing"
|
|
||||||
android:text="@string/continue_2" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/termsTextView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/onboarding_button_bottom_offset"
|
|
||||||
android:layout_marginLeft="@dimen/massive_spacing"
|
|
||||||
android:layout_marginRight="@dimen/massive_spacing"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textColorLink="?android:textColorPrimary"
|
|
||||||
android:textSize="@dimen/very_small_font_size"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
android:text="By using this service, you agree to our Terms of Service and Privacy Policy"
|
|
||||||
tools:ignore="HardcodedText" /> <!-- Intentionally not yet translated -->
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -47,7 +47,11 @@
|
|||||||
android:paddingTop="12dp"
|
android:paddingTop="12dp"
|
||||||
android:paddingBottom="12dp"
|
android:paddingBottom="12dp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
android:hint="@string/activity_settings_display_name_edit_text_hint" />
|
android:hint="@string/activity_settings_display_name_edit_text_hint"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textCapWords"
|
||||||
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/btnGroupNameDisplay"
|
android:id="@+id/btnGroupNameDisplay"
|
||||||
@ -55,8 +59,10 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:contentDescription="@string/AccessibilityId_username"
|
android:contentDescription="@string/AccessibilityId_username"
|
||||||
|
android:gravity="center"
|
||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="@dimen/very_large_font_size"
|
android:textSize="@dimen/very_large_font_size"
|
||||||
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:elevation="4dp"
|
android:elevation="4dp">
|
||||||
android:padding="@dimen/medium_spacing">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -21,6 +20,8 @@
|
|||||||
android:id="@+id/dialogDescriptionText"
|
android:id="@+id/dialogDescriptionText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
android:layout_marginTop="@dimen/large_spacing"
|
android:layout_marginTop="@dimen/large_spacing"
|
||||||
android:text="@string/dialog_clear_all_data_message"
|
android:text="@string/dialog_clear_all_data_message"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
@ -46,16 +47,15 @@
|
|||||||
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
||||||
android:id="@+id/clearAllDataButton"
|
android:id="@+id/clearAllDataButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="@dimen/medium_spacing"
|
|
||||||
android:text="@string/dialog_clear_all_data_clear" />
|
android:text="@string/dialog_clear_all_data_clear" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||||
android:id="@+id/cancelButton"
|
android:id="@+id/cancelButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/cancel" />
|
android:text="@string/cancel" />
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||||
android:id="@+id/cancelButton"
|
android:id="@+id/cancelButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/cancel" />
|
android:text="@string/cancel" />
|
||||||
|
|
||||||
@ -46,7 +46,7 @@
|
|||||||
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
||||||
android:id="@+id/sendSeedButton"
|
android:id="@+id/sendSeedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="@dimen/medium_spacing"
|
android:layout_marginStart="@dimen/medium_spacing"
|
||||||
android:text="@string/dialog_send_seed_send_button_title" />
|
android:text="@string/dialog_send_seed_send_button_title" />
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/export_logs_button"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
style="@style/Widget.Session.Button.Common.Filled"
|
style="@style/Widget.Session.Button.Common.Filled"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
@ -12,4 +13,5 @@
|
|||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -62,10 +62,14 @@
|
|||||||
android:layout_marginBottom="@dimen/medium_spacing"
|
android:layout_marginBottom="@dimen/medium_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_group_name_input"
|
android:contentDescription="@string/AccessibilityId_group_name_input"
|
||||||
android:hint="@string/activity_create_closed_group_edit_text_hint"
|
android:hint="@string/activity_create_closed_group_edit_text_hint"
|
||||||
android:maxLength="30"
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textCapWords"
|
||||||
|
android:maxLength="@integer/max_group_and_community_name_length_chars"
|
||||||
|
android:maxLines="1"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
app:layout_constraintTop_toBottomOf="@id/titleText"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||||
android:id="@+id/contactSearch"
|
android:id="@+id/contactSearch"
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
android:id="@+id/nameTextViewContainer"
|
android:id="@+id/nameTextViewContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/medium_spacing"
|
||||||
|
android:paddingEnd="@dimen/medium_spacing"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
@ -42,6 +44,7 @@
|
|||||||
android:id="@+id/nameTextView"
|
android:id="@+id/nameTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginStart="@dimen/small_spacing"
|
android:layout_marginStart="@dimen/small_spacing"
|
||||||
android:layout_marginEnd="@dimen/small_spacing"
|
android:layout_marginEnd="@dimen/small_spacing"
|
||||||
@ -57,6 +60,7 @@
|
|||||||
android:layout_height="22dp"
|
android:layout_height="22dp"
|
||||||
android:contentDescription="@string/AccessibilityId_edit_user_nickname"
|
android:contentDescription="@string/AccessibilityId_edit_user_nickname"
|
||||||
android:paddingTop="2dp"
|
android:paddingTop="2dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
android:src="@drawable/ic_baseline_edit_24" />
|
android:src="@drawable/ic_baseline_edit_24" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -73,6 +77,7 @@
|
|||||||
android:id="@+id/cancelNicknameEditingButton"
|
android:id="@+id/cancelNicknameEditingButton"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginLeft="@dimen/large_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_cancel"
|
android:contentDescription="@string/AccessibilityId_cancel"
|
||||||
android:src="@drawable/ic_baseline_clear_24" />
|
android:src="@drawable/ic_baseline_clear_24" />
|
||||||
|
|
||||||
@ -82,12 +87,12 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginHorizontal="@dimen/small_spacing"
|
|
||||||
android:contentDescription="@string/AccessibilityId_username"
|
android:contentDescription="@string/AccessibilityId_username"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:singleLine="true"
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
|
android:maxLines="1"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
android:textColorHint="?android:textColorSecondary"
|
android:textColorHint="?android:textColorSecondary"
|
||||||
android:hint="@string/fragment_user_details_bottom_sheet_edit_text_hint" />
|
android:hint="@string/fragment_user_details_bottom_sheet_edit_text_hint" />
|
||||||
@ -96,6 +101,7 @@
|
|||||||
android:id="@+id/saveNicknameButton"
|
android:id="@+id/saveNicknameButton"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginRight="@dimen/large_spacing"
|
||||||
android:contentDescription="@string/AccessibilityId_apply"
|
android:contentDescription="@string/AccessibilityId_apply"
|
||||||
android:src="@drawable/ic_baseline_done_24" />
|
android:src="@drawable/ic_baseline_done_24" />
|
||||||
|
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/container"
|
android:id="@+id/export_progress_container"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="16dp"
|
android:layout_gravity="bottom" >
|
||||||
android:gravity="bottom">
|
|
||||||
|
|
||||||
<ProgressBar android:id="@+id/progress_bar"
|
<ProgressBar
|
||||||
|
android:id="@+id/export_progress_bar"
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"/>
|
android:indeterminate="true"
|
||||||
|
android:visibility="invisible" />
|
||||||
<TextView android:id="@+id/progress_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
tools:text="1345 messages so far"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
@ -15,7 +15,7 @@
|
|||||||
android:layout_width="17dp"
|
android:layout_width="17dp"
|
||||||
android:layout_height="17dp" />
|
android:layout_height="17dp" />
|
||||||
|
|
||||||
<Space
|
<View
|
||||||
android:id="@+id/reactions_pill_spacer"
|
android:id="@+id/reactions_pill_spacer"
|
||||||
android:layout_width="4dp"
|
android:layout_width="4dp"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/medium_spacing"
|
android:layout_marginStart="@dimen/medium_spacing"
|
||||||
|
android:maxLength="@integer/max_user_nickname_length_chars"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
tools:text="@string/MessageRecord_you_disabled_disappearing_messages" />
|
tools:text="You disabled disappearing messages" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/call_view"
|
android:id="@+id/call_view"
|
||||||
|
@ -165,7 +165,7 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="@dimen/medium_font_size"
|
android:textSize="@dimen/medium_font_size"
|
||||||
tools:text="Sorry, gotta go fight crime again" />
|
tools:text="Sorry, gotta go fight crime again - and more text to make it ellipsize" />
|
||||||
|
|
||||||
<include layout="@layout/view_typing_indicator"
|
<include layout="@layout/view_typing_indicator"
|
||||||
android:id="@+id/typingIndicatorView"
|
android:id="@+id/typingIndicatorView"
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<dimen name="massive_font_size">50sp</dimen>
|
<dimen name="massive_font_size">50sp</dimen>
|
||||||
|
|
||||||
<!-- Element Sizes -->
|
<!-- Element Sizes -->
|
||||||
|
<dimen name="dialog_button_height">60dp</dimen>
|
||||||
<dimen name="small_button_height">34dp</dimen>
|
<dimen name="small_button_height">34dp</dimen>
|
||||||
<dimen name="medium_button_height">38dp</dimen>
|
<dimen name="medium_button_height">38dp</dimen>
|
||||||
<dimen name="large_button_height">54dp</dimen>
|
<dimen name="large_button_height">54dp</dimen>
|
||||||
|
@ -6,4 +6,7 @@
|
|||||||
<integer name="reaction_scrubber_reveal_offset">100</integer>
|
<integer name="reaction_scrubber_reveal_offset">100</integer>
|
||||||
<integer name="reaction_scrubber_hide_duration">150</integer>
|
<integer name="reaction_scrubber_hide_duration">150</integer>
|
||||||
<integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</integer>
|
<integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</integer>
|
||||||
|
|
||||||
|
<integer name="max_user_nickname_length_chars">35</integer>
|
||||||
|
<integer name="max_group_and_community_name_length_chars">35</integer>
|
||||||
</resources>
|
</resources>
|
@ -119,6 +119,7 @@
|
|||||||
<item name="android:textAllCaps">false</item>
|
<item name="android:textAllCaps">false</item>
|
||||||
<item name="android:textSize">@dimen/small_font_size</item>
|
<item name="android:textSize">@dimen/small_font_size</item>
|
||||||
<item name="android:textColor">?android:textColorPrimary</item>
|
<item name="android:textColor">?android:textColorPrimary</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Session.Button.Dialog.UnimportantText">
|
<style name="Widget.Session.Button.Dialog.UnimportantText">
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
<PreferenceCategory android:title="@string/preferences__link_previews">
|
<PreferenceCategory android:title="@string/preferences__link_previews">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
android:defaultValue="true"
|
android:defaultValue="false"
|
||||||
android:key="pref_link_previews"
|
android:key="pref_link_previews"
|
||||||
android:summary="@string/preferences__link_previews_summary"
|
android:summary="@string/preferences__link_previews_summary"
|
||||||
android:title="@string/preferences__send_link_previews"/>
|
android:title="@string/preferences__send_link_previews"/>
|
||||||
|
@ -7,38 +7,37 @@
|
|||||||
android:title="@string/activity_help_settings__report_bug_title"
|
android:title="@string/activity_help_settings__report_bug_title"
|
||||||
android:summary="@string/activity_help_settings__report_bug_summary"
|
android:summary="@string/activity_help_settings__report_bug_summary"
|
||||||
android:widgetLayout="@layout/export_logs_widget" />
|
android:widgetLayout="@layout/export_logs_widget" />
|
||||||
|
|
||||||
|
<!-- Note: Having this as `android:layout` rather than `android:layoutWidget` allows it to fit the screen width -->
|
||||||
|
<Preference android:layout="@layout/preference_widget_progress" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="translate_session"
|
android:key="translate_session"
|
||||||
android:title="@string/activity_help_settings__translate_session"
|
android:title="@string/activity_help_settings__translate_session"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="feedback"
|
android:key="feedback"
|
||||||
android:title="@string/activity_help_settings__feedback"
|
android:title="@string/activity_help_settings__feedback"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="faq"
|
android:key="faq"
|
||||||
android:title="@string/activity_help_settings__faq"
|
android:title="@string/activity_help_settings__faq"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory>
|
<PreferenceCategory>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="support"
|
android:key="support"
|
||||||
android:title="@string/activity_help_settings__support"
|
android:title="@string/activity_help_settings__support"
|
||||||
android:widgetLayout="@layout/preference_external_link"
|
android:widgetLayout="@layout/preference_external_link" />
|
||||||
/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
@ -0,0 +1,19 @@
|
|||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import org.session.libsignal.utilities.Log.Logger
|
||||||
|
|
||||||
|
object NoOpLogger: Logger() {
|
||||||
|
override fun v(tag: String?, message: String?, t: Throwable?) {}
|
||||||
|
|
||||||
|
override fun d(tag: String?, message: String?, t: Throwable?) {}
|
||||||
|
|
||||||
|
override fun i(tag: String?, message: String?, t: Throwable?) {}
|
||||||
|
|
||||||
|
override fun w(tag: String?, message: String?, t: Throwable?) {}
|
||||||
|
|
||||||
|
override fun e(tag: String?, message: String?, t: Throwable?) {}
|
||||||
|
|
||||||
|
override fun wtf(tag: String?, message: String?, t: Throwable?) {}
|
||||||
|
|
||||||
|
override fun blockUntilAllWritesFinished() {}
|
||||||
|
}
|
@ -1,10 +1,20 @@
|
|||||||
package org.thoughtcrime.securesms
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import org.junit.BeforeClass
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
open class BaseViewModelTest: BaseCoroutineTest() {
|
open class BaseViewModelTest: BaseCoroutineTest() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun setupLogger() {
|
||||||
|
Log.initialize(NoOpLogger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var instantExecutorRule = InstantTaskExecutorRule()
|
var instantExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ import kotlin.time.Duration.Companion.minutes
|
|||||||
private const val THREAD_ID = 1L
|
private const val THREAD_ID = 1L
|
||||||
private const val LOCAL_NUMBER = "05---local---address"
|
private const val LOCAL_NUMBER = "05---local---address"
|
||||||
private val LOCAL_ADDRESS = Address.fromSerialized(LOCAL_NUMBER)
|
private val LOCAL_ADDRESS = Address.fromSerialized(LOCAL_NUMBER)
|
||||||
private const val GROUP_NUMBER = "${GroupUtil.OPEN_GROUP_PREFIX}4133"
|
private const val GROUP_NUMBER = "${GroupUtil.COMMUNITY_PREFIX}4133"
|
||||||
private val GROUP_ADDRESS = Address.fromSerialized(GROUP_NUMBER)
|
private val GROUP_ADDRESS = Address.fromSerialized(GROUP_NUMBER)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@ -53,6 +53,7 @@ class DisappearingMessagesViewModelTest {
|
|||||||
@Mock lateinit var application: Application
|
@Mock lateinit var application: Application
|
||||||
@Mock lateinit var textSecurePreferences: TextSecurePreferences
|
@Mock lateinit var textSecurePreferences: TextSecurePreferences
|
||||||
@Mock lateinit var messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol
|
@Mock lateinit var messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol
|
||||||
|
@Mock lateinit var disappearingMessages: DisappearingMessages
|
||||||
@Mock lateinit var threadDb: ThreadDatabase
|
@Mock lateinit var threadDb: ThreadDatabase
|
||||||
@Mock lateinit var groupDb: GroupDatabase
|
@Mock lateinit var groupDb: GroupDatabase
|
||||||
@Mock lateinit var storage: Storage
|
@Mock lateinit var storage: Storage
|
||||||
@ -114,9 +115,9 @@ class DisappearingMessagesViewModelTest {
|
|||||||
isSelfAdmin = true,
|
isSelfAdmin = true,
|
||||||
address = LOCAL_ADDRESS,
|
address = LOCAL_ADDRESS,
|
||||||
isNoteToSelf = true,
|
isNoteToSelf = true,
|
||||||
expiryMode = ExpiryMode.NONE,
|
expiryMode = ExpiryMode.Legacy(0),
|
||||||
isNewConfigEnabled = false,
|
isNewConfigEnabled = false,
|
||||||
persistedMode = ExpiryMode.NONE,
|
persistedMode = ExpiryMode.Legacy(0),
|
||||||
showDebugOptions = false
|
showDebugOptions = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -127,7 +128,7 @@ class DisappearingMessagesViewModelTest {
|
|||||||
UiState(
|
UiState(
|
||||||
OptionsCard(
|
OptionsCard(
|
||||||
R.string.activity_disappearing_messages_timer,
|
R.string.activity_disappearing_messages_timer,
|
||||||
typeOption(ExpiryMode.NONE, selected = true),
|
typeOption(ExpiryMode.NONE, selected = false),
|
||||||
timeOption(ExpiryType.LEGACY, 12.hours),
|
timeOption(ExpiryType.LEGACY, 12.hours),
|
||||||
timeOption(ExpiryType.LEGACY, 1.days),
|
timeOption(ExpiryType.LEGACY, 1.days),
|
||||||
timeOption(ExpiryType.LEGACY, 7.days),
|
timeOption(ExpiryType.LEGACY, 7.days),
|
||||||
@ -555,6 +556,7 @@ class DisappearingMessagesViewModelTest {
|
|||||||
application,
|
application,
|
||||||
textSecurePreferences,
|
textSecurePreferences,
|
||||||
messageExpirationManager,
|
messageExpirationManager,
|
||||||
|
disappearingMessages,
|
||||||
threadDb,
|
threadDb,
|
||||||
groupDb,
|
groupDb,
|
||||||
storage,
|
storage,
|
||||||
|
@ -3,12 +3,14 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.hamcrest.CoreMatchers.endsWith
|
import org.hamcrest.CoreMatchers.endsWith
|
||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
import org.hamcrest.CoreMatchers.notNullValue
|
import org.hamcrest.CoreMatchers.notNullValue
|
||||||
import org.hamcrest.CoreMatchers.nullValue
|
import org.hamcrest.CoreMatchers.nullValue
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.BeforeClass
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.Mockito.anyLong
|
import org.mockito.Mockito.anyLong
|
||||||
@ -18,7 +20,9 @@ import org.mockito.kotlin.any
|
|||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.BaseViewModelTest
|
import org.thoughtcrime.securesms.BaseViewModelTest
|
||||||
|
import org.thoughtcrime.securesms.NoOpLogger
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
@ -32,6 +36,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
private val threadId = 123L
|
private val threadId = 123L
|
||||||
private val edKeyPair = mock<KeyPair>()
|
private val edKeyPair = mock<KeyPair>()
|
||||||
private lateinit var recipient: Recipient
|
private lateinit var recipient: Recipient
|
||||||
|
private lateinit var messageRecord: MessageRecord
|
||||||
|
|
||||||
private val viewModel: ConversationViewModel by lazy {
|
private val viewModel: ConversationViewModel by lazy {
|
||||||
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
||||||
@ -40,6 +45,9 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
recipient = mock()
|
recipient = mock()
|
||||||
|
messageRecord = mock { record ->
|
||||||
|
whenever(record.individualRecipient).thenReturn(recipient)
|
||||||
|
}
|
||||||
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
|
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
|
||||||
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
|
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
|
||||||
}
|
}
|
||||||
@ -144,7 +152,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
val error = Throwable()
|
val error = Throwable()
|
||||||
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error))
|
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Failure(error))
|
||||||
|
|
||||||
viewModel.banAndDeleteAll(recipient)
|
viewModel.banAndDeleteAll(messageRecord)
|
||||||
|
|
||||||
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
|
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
|
||||||
}
|
}
|
||||||
@ -153,7 +161,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
fun `should emit a message on ban user and delete all success`() = runBlockingTest {
|
fun `should emit a message on ban user and delete all success`() = runBlockingTest {
|
||||||
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
|
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(ResultOf.Success(Unit))
|
||||||
|
|
||||||
viewModel.banAndDeleteAll(recipient)
|
viewModel.banAndDeleteAll(messageRecord)
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
viewModel.uiState.first().uiMessages.first().message,
|
viewModel.uiState.first().uiMessages.first().message,
|
||||||
@ -189,7 +197,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `open group recipient should have no blinded recipient`() {
|
fun `open group recipient should have no blinded recipient`() {
|
||||||
whenever(recipient.isOpenGroupRecipient).thenReturn(true)
|
whenever(recipient.isCommunityRecipient).thenReturn(true)
|
||||||
whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false)
|
whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false)
|
||||||
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
|
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
|
||||||
assertThat(viewModel.blindedRecipient, nullValue())
|
assertThat(viewModel.blindedRecipient, nullValue())
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user