mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-20 12:58:26 +00:00
Merge branch 'dev' into just-prefs
This commit is contained in:
commit
268644edd2
@ -31,8 +31,8 @@ configurations.all {
|
||||
exclude module: "commons-logging"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 373
|
||||
def canonicalVersionName = "1.18.4"
|
||||
def canonicalVersionCode = 374
|
||||
def canonicalVersionName = "1.18.5"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
@ -271,7 +271,7 @@ dependencies {
|
||||
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
@ -322,6 +322,7 @@ dependencies {
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation "com.squareup.phrase:phrase:$phraseVersion"
|
||||
implementation 'app.cash.copper:copper-flow:1.0.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
|
@ -136,29 +136,29 @@ class SodiumUtilitiesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionIdSuccess() {
|
||||
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||
fun accountIdSuccess() {
|
||||
val result = SodiumUtilities.accountId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionIdFailureInvalidAccountId() {
|
||||
val result = SodiumUtilities.sessionId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||
fun accountIdFailureInvalidAccountId() {
|
||||
val result = SodiumUtilities.accountId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionIdFailureInvalidBlindedId() {
|
||||
val result = SodiumUtilities.sessionId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
|
||||
fun accountIdFailureInvalidBlindedId() {
|
||||
val result = SodiumUtilities.accountId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionIdFailureBlindingFactor() {
|
||||
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", "Test")
|
||||
fun accountIdFailureBlindingFactor() {
|
||||
val result = SodiumUtilities.accountId("05$publicKey", "15$blindedPublicKey", "Test")
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
@ -125,7 +125,7 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.home.HomeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTask"
|
||||
android:launchMode="standard"
|
||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
226074,en,AF,Africa,UG,Uganda,0
|
||||
239880,en,AF,Africa,CF,"Central African Republic",0
|
||||
241170,en,AF,Africa,SC,Seychelles,0
|
||||
248816,en,AS,Asia,JO,"Hashemite Kingdom of Jordan",0
|
||||
248816,en,AS,Asia,JO,Jordan,0
|
||||
272103,en,AS,Asia,LB,Lebanon,0
|
||||
285570,en,AS,Asia,KW,Kuwait,0
|
||||
286963,en,AS,Asia,OM,Oman,0
|
||||
@ -23,7 +23,7 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
290291,en,AS,Asia,BH,Bahrain,0
|
||||
290557,en,AS,Asia,AE,"United Arab Emirates",0
|
||||
294640,en,AS,Asia,IL,Israel,0
|
||||
298795,en,AS,Asia,TR,Turkey,0
|
||||
298795,en,AS,Asia,TR,Türkiye,0
|
||||
337996,en,AF,Africa,ET,Ethiopia,0
|
||||
338010,en,AF,Africa,ER,Eritrea,0
|
||||
357994,en,AF,Africa,EG,Egypt,0
|
||||
@ -33,13 +33,13 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
453733,en,EU,Europe,EE,Estonia,1
|
||||
458258,en,EU,Europe,LV,Latvia,1
|
||||
587116,en,AS,Asia,AZ,Azerbaijan,0
|
||||
597427,en,EU,Europe,LT,"Republic of Lithuania",1
|
||||
597427,en,EU,Europe,LT,Lithuania,1
|
||||
607072,en,EU,Europe,SJ,"Svalbard and Jan Mayen",0
|
||||
614540,en,AS,Asia,GE,Georgia,0
|
||||
617790,en,EU,Europe,MD,"Republic of Moldova",0
|
||||
617790,en,EU,Europe,MD,Moldova,0
|
||||
630336,en,EU,Europe,BY,Belarus,0
|
||||
660013,en,EU,Europe,FI,Finland,1
|
||||
661882,en,EU,Europe,AX,"Åland",1
|
||||
661882,en,EU,Europe,AX,"Åland Islands",1
|
||||
690791,en,EU,Europe,UA,Ukraine,0
|
||||
718075,en,EU,Europe,MK,"North Macedonia",0
|
||||
719819,en,EU,Europe,HU,Hungary,1
|
||||
@ -77,8 +77,8 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
1522867,en,AS,Asia,KZ,Kazakhstan,0
|
||||
1527747,en,AS,Asia,KG,Kyrgyzstan,0
|
||||
1546748,en,AN,Antarctica,TF,"French Southern Territories",0
|
||||
1547314,en,AN,Antarctica,HM,"Heard Island and McDonald Islands",0
|
||||
1547376,en,AS,Asia,CC,"Cocos [Keeling] Islands",0
|
||||
1547314,en,AN,Antarctica,HM,"Heard and McDonald Islands",0
|
||||
1547376,en,AS,Asia,CC,"Cocos (Keeling) Islands",0
|
||||
1559582,en,OC,Oceania,PW,Palau,0
|
||||
1562822,en,AS,Asia,VN,Vietnam,0
|
||||
1605651,en,AS,Asia,TH,Thailand,0
|
||||
@ -97,7 +97,7 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
1873107,en,AS,Asia,KP,"North Korea",0
|
||||
1880251,en,AS,Asia,SG,Singapore,0
|
||||
1899402,en,OC,Oceania,CK,"Cook Islands",0
|
||||
1966436,en,OC,Oceania,TL,"East Timor",0
|
||||
1966436,en,OC,Oceania,TL,Timor-Leste,0
|
||||
2017370,en,EU,Europe,RU,Russia,0
|
||||
2029969,en,AS,Asia,MN,Mongolia,0
|
||||
2077456,en,OC,Oceania,AU,Australia,0
|
||||
@ -131,7 +131,7 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
2400553,en,AF,Africa,GA,Gabon,0
|
||||
2403846,en,AF,Africa,SL,"Sierra Leone",0
|
||||
2410758,en,AF,Africa,ST,"São Tomé and Príncipe",0
|
||||
2411586,en,EU,Europe,GI,Gibraltar,1
|
||||
2411586,en,EU,Europe,GI,Gibraltar,0
|
||||
2413451,en,AF,Africa,GM,Gambia,0
|
||||
2420477,en,AF,Africa,GN,Guinea,0
|
||||
2434508,en,AF,Africa,TD,Chad,0
|
||||
@ -146,10 +146,10 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
2622320,en,EU,Europe,FO,"Faroe Islands",0
|
||||
2623032,en,EU,Europe,DK,Denmark,1
|
||||
2629691,en,EU,Europe,IS,Iceland,0
|
||||
2635167,en,EU,Europe,GB,"United Kingdom",1
|
||||
2635167,en,EU,Europe,GB,"United Kingdom",0
|
||||
2658434,en,EU,Europe,CH,Switzerland,0
|
||||
2661886,en,EU,Europe,SE,Sweden,1
|
||||
2750405,en,EU,Europe,NL,Netherlands,1
|
||||
2750405,en,EU,Europe,NL,"The Netherlands",1
|
||||
2782113,en,EU,Europe,AT,Austria,1
|
||||
2802361,en,EU,Europe,BE,Belgium,1
|
||||
2921044,en,EU,Europe,DE,Germany,1
|
||||
@ -203,7 +203,7 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
3576916,en,NA,"North America",TC,"Turks and Caicos Islands",0
|
||||
3577279,en,NA,"North America",AW,Aruba,0
|
||||
3577718,en,NA,"North America",VG,"British Virgin Islands",0
|
||||
3577815,en,NA,"North America",VC,"Saint Vincent and the Grenadines",0
|
||||
3577815,en,NA,"North America",VC,"St Vincent and Grenadines",0
|
||||
3578097,en,NA,"North America",MS,Montserrat,0
|
||||
3578421,en,NA,"North America",MF,"Saint Martin",1
|
||||
3578476,en,NA,"North America",BL,"Saint Barthélemy",0
|
||||
@ -238,7 +238,7 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
|
||||
4043988,en,OC,Oceania,GU,Guam,0
|
||||
4566966,en,NA,"North America",PR,"Puerto Rico",0
|
||||
4796775,en,NA,"North America",VI,"U.S. Virgin Islands",0
|
||||
5854968,en,OC,Oceania,UM,"U.S. Minor Outlying Islands",0
|
||||
5854968,en,OC,Oceania,UM,"U.S. Outlying Islands",0
|
||||
5880801,en,OC,Oceania,AS,"American Samoa",0
|
||||
6251999,en,NA,"North America",CA,Canada,0
|
||||
6252001,en,NA,"North America",US,"United States",0
|
||||
|
|
@ -154,8 +154,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
public boolean newAccount = false;
|
||||
|
||||
public static ApplicationContext getInstance(Context context) {
|
||||
return (ApplicationContext) context.getApplicationContext();
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
package org.thoughtcrime.securesms.calls
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity.SENSOR_SERVICE
|
||||
import org.thoughtcrime.securesms.webrtc.Orientation
|
||||
import kotlin.math.asin
|
||||
|
||||
class OrientationManager(private val context: Context): SensorEventListener {
|
||||
private var sensorManager: SensorManager? = null
|
||||
private var rotationVectorSensor: Sensor? = null
|
||||
|
||||
private val _orientation = MutableStateFlow(Orientation.UNKNOWN)
|
||||
val orientation: StateFlow<Orientation> = _orientation
|
||||
|
||||
fun startOrientationListener(){
|
||||
// create the sensor manager if it's still null
|
||||
if(sensorManager == null) {
|
||||
sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager
|
||||
}
|
||||
|
||||
if(rotationVectorSensor == null) {
|
||||
rotationVectorSensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
||||
}
|
||||
|
||||
sensorManager?.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_UI)
|
||||
}
|
||||
|
||||
fun stopOrientationListener(){
|
||||
sensorManager?.unregisterListener(this)
|
||||
}
|
||||
|
||||
fun destroy(){
|
||||
stopOrientationListener()
|
||||
sensorManager = null
|
||||
rotationVectorSensor = null
|
||||
_orientation.value = Orientation.UNKNOWN
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
|
||||
// if auto-rotate is off, bail and send UNKNOWN
|
||||
if (!isAutoRotateOn()) {
|
||||
_orientation.value = Orientation.UNKNOWN
|
||||
return
|
||||
}
|
||||
|
||||
// Get the quaternion from the rotation vector sensor
|
||||
val quaternion = FloatArray(4)
|
||||
SensorManager.getQuaternionFromVector(quaternion, event.values)
|
||||
|
||||
// Calculate Euler angles from the quaternion
|
||||
val pitch = asin(2.0 * (quaternion[0] * quaternion[2] - quaternion[3] * quaternion[1]))
|
||||
|
||||
// Convert radians to degrees
|
||||
val pitchDegrees = Math.toDegrees(pitch).toFloat()
|
||||
|
||||
// Determine the device's orientation based on the pitch and roll values
|
||||
val currentOrientation = when {
|
||||
pitchDegrees > 45 -> Orientation.LANDSCAPE
|
||||
pitchDegrees < -45 -> Orientation.REVERSED_LANDSCAPE
|
||||
else -> Orientation.PORTRAIT
|
||||
}
|
||||
|
||||
if (currentOrientation != _orientation.value) {
|
||||
_orientation.value = currentOrientation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Function to check if Android System Auto-rotate is on or off
|
||||
private fun isAutoRotateOn(): Boolean {
|
||||
return Settings.System.getInt(
|
||||
context.contentResolver,
|
||||
Settings.System.ACCELEROMETER_ROTATION, 0
|
||||
) == 1
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||
}
|
@ -5,11 +5,17 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Outline
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorManager
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.MenuItem
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -21,13 +27,14 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import android.provider.Settings
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityWebrtcBinding
|
||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.truncateIdForDisplay
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
@ -43,8 +50,11 @@ import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_OUTGOING
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_PRE_INIT
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING
|
||||
import org.thoughtcrime.securesms.webrtc.Orientation
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.asin
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
@ -60,6 +70,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
private const val CALL_DURATION_FORMAT = "HH:mm:ss"
|
||||
}
|
||||
|
||||
@Inject lateinit var prefs: TextSecurePreferences
|
||||
|
||||
private val viewModel by viewModels<CallViewModel>()
|
||||
private val glide by lazy { GlideApp.with(this) }
|
||||
private lateinit var binding: ActivityWebrtcBinding
|
||||
@ -71,16 +83,13 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
private var hangupReceiver: BroadcastReceiver? = null
|
||||
|
||||
private val rotationListener by lazy {
|
||||
object : OrientationEventListener(this) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if ((orientation + 15) % 90 < 30) {
|
||||
viewModel.deviceRotation = orientation
|
||||
// updateControlsRotation(orientation.quadrantRotation() * -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* We need to track the device's orientation so we can calculate whether or not to rotate the video streams
|
||||
* This works a lot better than using `OrientationEventListener > onOrientationChanged'
|
||||
* which gives us a rotation angle that doesn't take into account pitch vs roll, so tipping the device from front to back would
|
||||
* trigger the video rotation logic, while we really only want it when the device is in portrait or landscape.
|
||||
*/
|
||||
private var orientationManager = OrientationManager(this)
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
@ -102,13 +111,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
||||
// Only enable auto-rotate if system auto-rotate is enabled
|
||||
if (isAutoRotateOn()) {
|
||||
rotationListener.enable()
|
||||
} else {
|
||||
rotationListener.disable()
|
||||
}
|
||||
|
||||
binding = ActivityWebrtcBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
@ -136,6 +138,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
}
|
||||
|
||||
binding.floatingRendererContainer.setOnClickListener {
|
||||
viewModel.swapVideos()
|
||||
}
|
||||
|
||||
binding.microphoneButton.setOnClickListener {
|
||||
val audioEnabledIntent =
|
||||
WebRtcCallService.microphoneIntent(this, !viewModel.microphoneEnabled)
|
||||
@ -174,7 +180,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.onAllGranted {
|
||||
val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoEnabled)
|
||||
val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoState.value.userVideoEnabled)
|
||||
startService(intent)
|
||||
}
|
||||
.execute()
|
||||
@ -191,14 +197,53 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
orientationManager.orientation.collect { orientation ->
|
||||
viewModel.deviceOrientation = orientation
|
||||
updateControlsRotation()
|
||||
}
|
||||
}
|
||||
|
||||
clipFloatingInsets()
|
||||
|
||||
// set up the user avatar
|
||||
prefs.getLocalNumber()?.let{
|
||||
binding.userAvatar.apply {
|
||||
publicKey = it
|
||||
displayName = prefs.getProfileName() ?: truncateIdForDisplay(it)
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Function to check if Android System Auto-rotate is on or off
|
||||
private fun isAutoRotateOn(): Boolean {
|
||||
return Settings.System.getInt(
|
||||
contentResolver,
|
||||
Settings.System.ACCELEROMETER_ROTATION, 0
|
||||
) == 1
|
||||
/**
|
||||
* Makes sure the floating video inset has clipped rounded corners, included with the video stream itself
|
||||
*/
|
||||
private fun clipFloatingInsets() {
|
||||
// clip the video inset with rounded corners
|
||||
val videoInsetProvider = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
// all corners
|
||||
outline.setRoundRect(
|
||||
0, 0, view.width, view.height,
|
||||
resources.getDimensionPixelSize(R.dimen.video_inset_radius).toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
binding.floatingRendererContainer.outlineProvider = videoInsetProvider
|
||||
binding.floatingRendererContainer.clipToOutline = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
orientationManager.startOrientationListener()
|
||||
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
orientationManager.stopOrientationListener()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -206,7 +251,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
hangupReceiver?.let { receiver ->
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
||||
}
|
||||
rotationListener.disable()
|
||||
|
||||
orientationManager.destroy()
|
||||
}
|
||||
|
||||
private fun answerCall() {
|
||||
@ -214,15 +260,33 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
ContextCompat.startForegroundService(this, answerIntent)
|
||||
}
|
||||
|
||||
private fun updateControlsRotation(newRotation: Int) {
|
||||
private fun updateControlsRotation() {
|
||||
with (binding) {
|
||||
val rotation = newRotation.toFloat()
|
||||
remoteRecipient.rotation = rotation
|
||||
speakerPhoneButton.rotation = rotation
|
||||
microphoneButton.rotation = rotation
|
||||
enableCameraButton.rotation = rotation
|
||||
switchCameraButton.rotation = rotation
|
||||
endCallButton.rotation = rotation
|
||||
val rotation = when(viewModel.deviceOrientation){
|
||||
Orientation.LANDSCAPE -> -90f
|
||||
Orientation.REVERSED_LANDSCAPE -> 90f
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
userAvatar.animate().cancel()
|
||||
userAvatar.animate().rotation(rotation).start()
|
||||
contactAvatar.animate().cancel()
|
||||
contactAvatar.animate().rotation(rotation).start()
|
||||
|
||||
speakerPhoneButton.animate().cancel()
|
||||
speakerPhoneButton.animate().rotation(rotation).start()
|
||||
|
||||
microphoneButton.animate().cancel()
|
||||
microphoneButton.animate().rotation(rotation).start()
|
||||
|
||||
enableCameraButton.animate().cancel()
|
||||
enableCameraButton.animate().rotation(rotation).start()
|
||||
|
||||
switchCameraButton.animate().cancel()
|
||||
switchCameraButton.animate().rotation(rotation).start()
|
||||
|
||||
endCallButton.animate().cancel()
|
||||
endCallButton.animate().rotation(rotation).start()
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,44 +344,20 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
launch {
|
||||
viewModel.recipient.collect { latestRecipient ->
|
||||
binding.contactAvatar.recycle()
|
||||
|
||||
if (latestRecipient.recipient != null) {
|
||||
val publicKey = latestRecipient.recipient.address.serialize()
|
||||
val displayName = getUserDisplayName(publicKey)
|
||||
supportActionBar?.title = displayName
|
||||
val signalProfilePicture = latestRecipient.recipient.contactPhoto
|
||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||
val sizeInPX =
|
||||
resources.getDimensionPixelSize(R.dimen.extra_large_profile_picture_size)
|
||||
binding.remoteRecipientName.text = displayName
|
||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||
glide.clear(binding.remoteRecipient)
|
||||
glide.load(signalProfilePicture)
|
||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||
.circleCrop()
|
||||
.error(
|
||||
AvatarPlaceholderGenerator.generate(
|
||||
this@WebRtcCallActivity,
|
||||
sizeInPX,
|
||||
publicKey,
|
||||
displayName
|
||||
)
|
||||
)
|
||||
.into(binding.remoteRecipient)
|
||||
} else {
|
||||
glide.clear(binding.remoteRecipient)
|
||||
glide.load(
|
||||
AvatarPlaceholderGenerator.generate(
|
||||
this@WebRtcCallActivity,
|
||||
sizeInPX,
|
||||
publicKey,
|
||||
displayName
|
||||
)
|
||||
)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop()
|
||||
.into(binding.remoteRecipient)
|
||||
val contactPublicKey = latestRecipient.recipient.address.serialize()
|
||||
val contactDisplayName = getUserDisplayName(contactPublicKey)
|
||||
supportActionBar?.title = contactDisplayName
|
||||
binding.remoteRecipientName.text = contactDisplayName
|
||||
|
||||
// sort out the contact's avatar
|
||||
binding.contactAvatar.apply {
|
||||
publicKey = contactPublicKey
|
||||
displayName = contactDisplayName
|
||||
update()
|
||||
}
|
||||
} else {
|
||||
glide.clear(binding.remoteRecipient)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -346,39 +386,65 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// handle video state
|
||||
launch {
|
||||
viewModel.localVideoEnabledState.collect { isEnabled ->
|
||||
binding.localRenderer.removeAllViews()
|
||||
if (isEnabled) {
|
||||
viewModel.localRenderer?.let { surfaceView ->
|
||||
surfaceView.setZOrderOnTop(true)
|
||||
viewModel.videoState.collect { state ->
|
||||
binding.floatingRenderer.removeAllViews()
|
||||
binding.fullscreenRenderer.removeAllViews()
|
||||
|
||||
// Mirror the video preview of the person making the call to prevent disorienting them
|
||||
surfaceView.setMirror(true)
|
||||
|
||||
binding.localRenderer.addView(surfaceView)
|
||||
// handle fullscreen video window
|
||||
if(state.showFullscreenVideo()){
|
||||
viewModel.fullscreenRenderer?.let { surfaceView ->
|
||||
binding.fullscreenRenderer.addView(surfaceView)
|
||||
binding.fullscreenRenderer.isVisible = true
|
||||
hideAvatar()
|
||||
}
|
||||
} else {
|
||||
binding.fullscreenRenderer.isVisible = false
|
||||
showAvatar(state.swapped)
|
||||
}
|
||||
binding.localRenderer.isVisible = isEnabled
|
||||
binding.enableCameraButton.isSelected = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.remoteVideoEnabledState.collect { isEnabled ->
|
||||
binding.remoteRenderer.removeAllViews()
|
||||
if (isEnabled) {
|
||||
viewModel.remoteRenderer?.let { surfaceView ->
|
||||
binding.remoteRenderer.addView(surfaceView)
|
||||
// handle floating video window
|
||||
if(state.showFloatingVideo()){
|
||||
viewModel.floatingRenderer?.let { surfaceView ->
|
||||
binding.floatingRenderer.addView(surfaceView)
|
||||
binding.floatingRenderer.isVisible = true
|
||||
binding.swapViewIcon.bringToFront()
|
||||
}
|
||||
} else {
|
||||
binding.floatingRenderer.isVisible = false
|
||||
}
|
||||
binding.remoteRenderer.isVisible = isEnabled
|
||||
binding.remoteRecipient.isVisible = !isEnabled
|
||||
|
||||
// the floating video inset (empty or not) should be shown
|
||||
// the moment we have either of the video streams
|
||||
val showFloatingContainer = state.userVideoEnabled || state.remoteVideoEnabled
|
||||
binding.floatingRendererContainer.isVisible = showFloatingContainer
|
||||
binding.swapViewIcon.isVisible = showFloatingContainer
|
||||
|
||||
// make sure to default to the contact's avatar if the floating container is not visible
|
||||
if (!showFloatingContainer) showAvatar(false)
|
||||
|
||||
// handle buttons
|
||||
binding.enableCameraButton.isSelected = state.userVideoEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the avatar image.
|
||||
* If @showUserAvatar is true, the user's avatar is shown, otherwise the contact's avatar is shown.
|
||||
*/
|
||||
private fun showAvatar(showUserAvatar: Boolean) {
|
||||
binding.userAvatar.isVisible = showUserAvatar
|
||||
binding.contactAvatar.isVisible = !showUserAvatar
|
||||
}
|
||||
|
||||
private fun hideAvatar() {
|
||||
binding.userAvatar.isVisible = false
|
||||
binding.contactAvatar.isVisible = false
|
||||
}
|
||||
|
||||
private fun getUserDisplayName(publicKey: String): String {
|
||||
val contact =
|
||||
DatabaseComponent.get(this).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||
@ -388,7 +454,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
uiJob?.cancel()
|
||||
binding.remoteRenderer.removeAllViews()
|
||||
binding.localRenderer.removeAllViews()
|
||||
binding.fullscreenRenderer.removeAllViews()
|
||||
binding.floatingRenderer.removeAllViews()
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
@ -25,6 +26,8 @@ import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
class ProfilePictureView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : RelativeLayout(context, attrs) {
|
||||
private val TAG = "ProfilePictureView"
|
||||
|
||||
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
||||
private val glide: GlideRequests = GlideApp.with(this)
|
||||
private val prefs = TextSecurePreferences(context)
|
||||
@ -33,7 +36,6 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
var displayName: String? = null
|
||||
var additionalPublicKey: String? = null
|
||||
var additionalDisplayName: String? = null
|
||||
var isLarge = false
|
||||
|
||||
private val profilePicturesCache = mutableMapOf<View, Recipient>()
|
||||
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||
@ -91,31 +93,27 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun update() {
|
||||
val publicKey = publicKey ?: return
|
||||
val publicKey = publicKey ?: return Log.w(TAG, "Could not find public key to update profile picture")
|
||||
val additionalPublicKey = additionalPublicKey
|
||||
// if we have a multi avatar setup
|
||||
if (additionalPublicKey != null) {
|
||||
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
|
||||
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
|
||||
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
|
||||
} else {
|
||||
|
||||
// clear single image
|
||||
glide.clear(binding.singleModeImageView)
|
||||
binding.singleModeImageView.visibility = View.INVISIBLE
|
||||
} else { // single image mode
|
||||
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
|
||||
binding.singleModeImageView.visibility = View.VISIBLE
|
||||
|
||||
// clear multi image
|
||||
glide.clear(binding.doubleModeImageView1)
|
||||
glide.clear(binding.doubleModeImageView2)
|
||||
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||
}
|
||||
if (additionalPublicKey == null && !isLarge) {
|
||||
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
|
||||
binding.singleModeImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(binding.singleModeImageView)
|
||||
binding.singleModeImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
if (additionalPublicKey == null && isLarge) {
|
||||
setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName)
|
||||
binding.largeSingleModeImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
glide.clear(binding.largeSingleModeImageView)
|
||||
binding.largeSingleModeImageView.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {
|
||||
|
@ -49,17 +49,20 @@ internal fun StartConversationScreen(
|
||||
ItemButton(
|
||||
textId = R.string.messageNew,
|
||||
icon = R.drawable.ic_message,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
|
||||
onClick = delegate::onNewMessageSelected)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
ItemButton(
|
||||
textId = R.string.activity_create_group_title,
|
||||
icon = R.drawable.ic_group,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
|
||||
onClick = delegate::onCreateGroupSelected
|
||||
)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
ItemButton(
|
||||
textId = R.string.dialog_join_community_title,
|
||||
icon = R.drawable.ic_globe,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
|
||||
onClick = delegate::onJoinCommunitySelected
|
||||
)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
|
@ -85,8 +85,8 @@ private fun EnterAccountId(
|
||||
SessionOutlinedTextField(
|
||||
text = state.newMessageIdOrOns,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = LocalDimensions.current.smallMargin)
|
||||
.contentDescription("Session id input box"),
|
||||
.padding(horizontal = LocalDimensions.current.smallMargin),
|
||||
contentDescription = "Session id input box",
|
||||
placeholder = stringResource(R.string.accountIdOrOnsEnter),
|
||||
onChange = callbacks::onChange,
|
||||
onContinue = callbacks::onContinue,
|
||||
|
@ -45,7 +45,7 @@ class NewMessageFragment : Fragment() {
|
||||
viewModel,
|
||||
onClose = { delegate.onDialogClosePressed() },
|
||||
onBack = { delegate.onDialogBackPressed() },
|
||||
onHelp = { requireContext().openUrl("https://sessionapp.zendesk.com/hc/en-us/articles/4439132747033-How-do-Session-ID-usernames-work") }
|
||||
onHelp = { requireContext().openUrl("https://sessionapp.zendesk.com/hc/en-us/articles/4439132747033-How-do-Account-ID-usernames-work") }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -239,12 +239,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
intent.getParcelableExtra<Address>(ADDRESS)?.let { it ->
|
||||
threadId = threadDb.getThreadIdIfExistsFor(it.serialize())
|
||||
if (threadId == -1L) {
|
||||
val sessionId = AccountId(it.serialize())
|
||||
val accountId = AccountId(it.serialize())
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(intent.getLongExtra(FROM_GROUP_THREAD_ID, -1))
|
||||
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
|
||||
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
|
||||
val address = if (accountId.prefix == IdPrefix.BLINDED && openGroup != null) {
|
||||
storage.getOrCreateBlindedIdMapping(accountId.hexString, openGroup.server, openGroup.publicKey).accountId?.let {
|
||||
fromSerialized(it)
|
||||
} ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId)
|
||||
} ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, accountId)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
@ -1131,8 +1131,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
}
|
||||
|
||||
override fun copyAccountID(sessionId: String) {
|
||||
val clip = ClipData.newPlainText("Account ID", sessionId)
|
||||
override fun copyAccountID(accountId: String) {
|
||||
val clip = ClipData.newPlainText("Account ID", accountId)
|
||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
manager.setPrimaryClip(clip)
|
||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
|
@ -2,19 +2,16 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.PointF
|
||||
import android.net.Uri
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isGone
|
||||
@ -33,11 +30,8 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.SimpleTextWatcher
|
||||
import org.thoughtcrime.securesms.util.addTextChangedListener
|
||||
import org.thoughtcrime.securesms.util.contains
|
||||
import org.thoughtcrime.securesms.util.toDp
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
|
||||
// Enums to keep track of the state of our voice recording mechanism as the user can
|
||||
// manipulate the UI faster than we can setup & teardown.
|
||||
@ -48,16 +42,24 @@ enum class VoiceRecorderState {
|
||||
ShuttingDownAfterRecord
|
||||
}
|
||||
|
||||
class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate,
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
class InputBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : RelativeLayout(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
), InputBarEditTextDelegate,
|
||||
QuoteViewDelegate,
|
||||
LinkPreviewDraftViewDelegate,
|
||||
TextView.OnEditorActionListener {
|
||||
private lateinit var binding: ViewInputBarBinding
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private val vMargin by lazy { toDp(4, resources) }
|
||||
private val minHeight by lazy { toPx(56, resources) }
|
||||
|
||||
private var binding: ViewInputBarBinding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
private var linkPreviewDraftView: LinkPreviewDraftView? = null
|
||||
private var quoteView: QuoteView? = null
|
||||
var delegate: InputBarDelegate? = null
|
||||
var additionalContentHeight = 0
|
||||
var quote: MessageRecord? = null
|
||||
var linkPreview: LinkPreview? = null
|
||||
var showInput: Boolean = true
|
||||
@ -70,7 +72,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
}
|
||||
|
||||
var text: String
|
||||
get() { return binding.inputBarEditText.text?.toString() ?: "" }
|
||||
get() = binding.inputBarEditText.text?.toString() ?: ""
|
||||
set(value) { binding.inputBarEditText.setText(value) }
|
||||
|
||||
// Keep track of when the user pressed the record voice message button, the duration that
|
||||
@ -79,21 +81,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
var voiceMessageDurationMS = 0L
|
||||
var voiceRecorderState = VoiceRecorderState.Idle
|
||||
|
||||
val attachmentButtonsContainerHeight: Int
|
||||
get() = binding.attachmentsButtonContainer.height
|
||||
private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)}
|
||||
val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)}
|
||||
private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)}
|
||||
|
||||
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)} }
|
||||
private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)} }
|
||||
private val sendButton by lazy { InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)} }
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun initialize() {
|
||||
binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
init {
|
||||
// Attachments button
|
||||
binding.attachmentsButtonContainer.addView(attachmentsButton)
|
||||
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
@ -112,6 +104,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
// `microphoneButton.onUp` and tap the button then the logged output order is onUp and THEN onPress!
|
||||
microphoneButton.setOnTouchListener(object : OnTouchListener {
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
if (!microphoneButton.snIsEnabled) return true
|
||||
|
||||
// We only handle single finger touch events so just consume the event and bail if there are more
|
||||
if (event.pointerCount > 1) return true
|
||||
@ -162,12 +155,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
binding.inputBarEditText.setOnEditorActionListener(this)
|
||||
if (context.prefs.isEnterSendsEnabled()) {
|
||||
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_SEND
|
||||
binding.inputBarEditText.inputType =
|
||||
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
binding.inputBarEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
} else {
|
||||
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_NONE
|
||||
binding.inputBarEditText.inputType =
|
||||
binding.inputBarEditText.inputType or
|
||||
binding.inputBarEditText.inputType
|
||||
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
}
|
||||
val incognitoFlag = if (context.prefs.isIncognitoKeyboardEnabled()) 16777216 else 0
|
||||
@ -184,9 +176,6 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
return false
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun inputBarEditTextContentChanged(text: CharSequence) {
|
||||
microphoneButton.isVisible = text.trim().isEmpty()
|
||||
sendButton.isVisible = microphoneButton.isGone
|
||||
@ -288,12 +277,9 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
||||
fun setInputBarEditableFactory(factory: Editable.Factory) {
|
||||
binding.inputBarEditText.setEditableFactory(factory)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
interface InputBarDelegate {
|
||||
|
||||
fun inputBarEditTextContentChanged(newContent: CharSequence)
|
||||
fun toggleAttachmentOptions()
|
||||
fun showVoiceMessageUI()
|
||||
@ -303,4 +289,4 @@ interface InputBarDelegate {
|
||||
fun onMicrophoneButtonUp(event: MotionEvent)
|
||||
fun sendMessage()
|
||||
fun commitInputContent(contentUri: Uri)
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ object ConversationMenuHelper {
|
||||
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
|
||||
inflater.inflate(R.menu.menu_conversation_expiration, menu)
|
||||
}
|
||||
// One-on-one chat menu allows copying the session id
|
||||
// One-on-one chat menu allows copying the account id
|
||||
if (thread.isContactRecipient) {
|
||||
inflater.inflate(R.menu.menu_conversation_copy_account_id, menu)
|
||||
}
|
||||
@ -325,7 +325,7 @@ object ConversationMenuHelper {
|
||||
interface ConversationMenuListener {
|
||||
fun block(deleteThread: Boolean = false)
|
||||
fun unblock()
|
||||
fun copyAccountID(sessionId: String)
|
||||
fun copyAccountID(accountId: String)
|
||||
fun copyOpenGroupUrl(thread: Recipient)
|
||||
fun showDisappearingMessages(thread: Recipient)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import androidx.core.view.isVisible
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
@ -289,7 +290,7 @@ class VisibleMessageContentView : ConstraintLayout {
|
||||
|
||||
// replace URLSpans with ModalURLSpans
|
||||
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
|
||||
val updatedUrl = urlSpan.url.let { HttpUrl.parse(it).toString() }
|
||||
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
|
||||
val replacementSpan = ModalURLSpan(updatedUrl) { url ->
|
||||
val activity = context as AppCompatActivity
|
||||
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")
|
||||
|
@ -1,29 +1,24 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Range
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.combine.Tuple2
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import org.session.libsession.utilities.prefs
|
||||
import org.session.libsession.utilities.truncateIdForDisplay
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.RoundedBackgroundSpan
|
||||
import org.thoughtcrime.securesms.util.getAccentColor
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object MentionUtilities {
|
||||
@ -69,7 +64,7 @@ object MentionUtilities {
|
||||
} else {
|
||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
|
||||
contact?.displayName(context)
|
||||
contact?.displayName(context) ?: truncateIdForDisplay(publicKey)
|
||||
}
|
||||
if (userDisplayName != null) {
|
||||
val mention = "@$userDisplayName"
|
||||
@ -162,7 +157,7 @@ object MentionUtilities {
|
||||
}
|
||||
|
||||
private fun isYou(mentionedPublicKey: String, userPublicKey: String, openGroup: OpenGroup?): Boolean {
|
||||
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.sessionId(userPublicKey, mentionedPublicKey, it.publicKey) } ?: false
|
||||
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.accountId(userPublicKey, mentionedPublicKey, it.publicKey) } ?: false
|
||||
return mentionedPublicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
||||
private fun readBlindedIdMapping(cursor: Cursor): BlindedIdMapping {
|
||||
return BlindedIdMapping(
|
||||
blindedId = cursor.getString(cursor.getColumnIndexOrThrow(BLINDED_PK)),
|
||||
sessionId = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(SESSION_PK)),
|
||||
accountId = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(SESSION_PK)),
|
||||
serverUrl = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_URL)),
|
||||
serverId = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_PK)),
|
||||
)
|
||||
@ -58,7 +58,7 @@ class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
||||
try {
|
||||
val values = ContentValues().apply {
|
||||
put(BLINDED_PK, blindedIdMapping.blindedId)
|
||||
put(SERVER_PK, blindedIdMapping.sessionId)
|
||||
put(SERVER_PK, blindedIdMapping.accountId)
|
||||
put(SERVER_URL, blindedIdMapping.serverUrl)
|
||||
put(SERVER_PK, blindedIdMapping.serverId)
|
||||
}
|
||||
|
@ -1243,73 +1243,49 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
}
|
||||
|
||||
private fun getNotificationMmsMessageRecord(cursor: Cursor): NotificationMmsMessageRecord {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
|
||||
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
||||
val dateReceived = cursor.getLong(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
NORMALIZED_DATE_RECEIVED
|
||||
)
|
||||
)
|
||||
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
||||
val mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
||||
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
||||
val addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(ADDRESS_DEVICE_ID))
|
||||
val recipient = getRecipientFor(address)
|
||||
val contentLocation = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION))
|
||||
val transactionId = cursor.getString(cursor.getColumnIndexOrThrow(TRANSACTION_ID))
|
||||
val messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_SIZE))
|
||||
val expiry = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRY))
|
||||
val status = cursor.getInt(cursor.getColumnIndexOrThrow(STATUS))
|
||||
val deliveryReceiptCount = cursor.getInt(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
DELIVERY_RECEIPT_COUNT
|
||||
)
|
||||
)
|
||||
val readReceiptCount = if (context.prefs.isReadReceiptsEnabled()) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0
|
||||
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
|
||||
val contentLocationBytes: ByteArray? = contentLocation?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
|
||||
val transactionIdBytes: ByteArray? = transactionId?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
|
||||
val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
|
||||
// Note: Additional details such as ADDRESS_DEVICE_ID, CONTENT_LOCATION, and TRANSACTION_ID are available if required.
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
|
||||
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
||||
val dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_RECEIVED))
|
||||
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
||||
val mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
||||
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
||||
val recipient = getRecipientFor(address)
|
||||
val messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_SIZE))
|
||||
val expiry = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRY))
|
||||
val status = cursor.getInt(cursor.getColumnIndexOrThrow(STATUS))
|
||||
val deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(DELIVERY_RECEIPT_COUNT))
|
||||
val readReceiptCount = if (context.prefs.isReadReceiptsEnabled()) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0
|
||||
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
|
||||
val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
|
||||
return NotificationMmsMessageRecord(
|
||||
id, recipient, recipient,
|
||||
dateSent, dateReceived, deliveryReceiptCount, threadId,
|
||||
contentLocationBytes, messageSize, expiry, status,
|
||||
transactionIdBytes, mailbox, slideDeck,
|
||||
messageSize, expiry, status, mailbox, slideDeck,
|
||||
readReceiptCount, hasMention
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMediaMmsMessageRecord(cursor: Cursor, getQuote: Boolean): MediaMmsMessageRecord {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
|
||||
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
||||
val dateReceived = cursor.getLong(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
NORMALIZED_DATE_RECEIVED
|
||||
)
|
||||
)
|
||||
val box = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
||||
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
||||
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
||||
val addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(ADDRESS_DEVICE_ID))
|
||||
val deliveryReceiptCount = cursor.getInt(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
DELIVERY_RECEIPT_COUNT
|
||||
)
|
||||
)
|
||||
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
|
||||
val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY))
|
||||
val partCount = cursor.getInt(cursor.getColumnIndexOrThrow(PART_COUNT))
|
||||
val mismatchDocument = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
MISMATCHED_IDENTITIES
|
||||
)
|
||||
)
|
||||
val networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(NETWORK_FAILURE))
|
||||
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
|
||||
val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN))
|
||||
val expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED))
|
||||
val unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1
|
||||
val hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
|
||||
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
||||
val dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_RECEIVED))
|
||||
val box = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
||||
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
||||
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
||||
val addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(ADDRESS_DEVICE_ID))
|
||||
val deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(DELIVERY_RECEIPT_COUNT))
|
||||
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
|
||||
val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY))
|
||||
val partCount = cursor.getInt(cursor.getColumnIndexOrThrow(PART_COUNT))
|
||||
val mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MISMATCHED_IDENTITIES))
|
||||
val networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(NETWORK_FAILURE))
|
||||
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
|
||||
val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN))
|
||||
val expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED))
|
||||
val unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1
|
||||
val hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1
|
||||
|
||||
if (!context.prefs.isReadReceiptsEnabled()) {
|
||||
readReceiptCount = 0
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public class RecipientDatabase extends Database {
|
||||
private static final String SYSTEM_PHONE_LABEL = "system_phone_label";
|
||||
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
|
||||
private static final String SIGNAL_PROFILE_NAME = "signal_profile_name";
|
||||
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
|
||||
private static final String SESSION_PROFILE_AVATAR = "signal_profile_avatar";
|
||||
private static final String PROFILE_SHARING = "profile_sharing_approval";
|
||||
private static final String CALL_RINGTONE = "call_ringtone";
|
||||
private static final String CALL_VIBRATE = "call_vibrate";
|
||||
@ -69,7 +69,7 @@ public class RecipientDatabase extends Database {
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
|
||||
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
|
||||
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
SIGNAL_PROFILE_NAME, SESSION_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS
|
||||
};
|
||||
@ -97,7 +97,7 @@ public class RecipientDatabase extends Database {
|
||||
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_KEY + " TEXT DEFAULT NULL, " +
|
||||
SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " +
|
||||
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
|
||||
SESSION_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_SHARING + " INTEGER DEFAULT 0, " +
|
||||
CALL_RINGTONE + " TEXT DEFAULT NULL, " +
|
||||
CALL_VIBRATE + " INTEGER DEFAULT " + Recipient.VibrateState.DEFAULT.getId() + ", " +
|
||||
@ -204,7 +204,7 @@ public class RecipientDatabase extends Database {
|
||||
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
|
||||
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
|
||||
String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME));
|
||||
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
|
||||
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SESSION_PROFILE_AVATAR));
|
||||
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
|
||||
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
|
||||
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
|
||||
@ -361,7 +361,7 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
public void setProfileAvatar(@NonNull Recipient recipient, @Nullable String profileAvatar) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar);
|
||||
contentValues.put(SESSION_PROFILE_AVATAR, profileAvatar);
|
||||
updateOrInsert(recipient.getAddress(), contentValues);
|
||||
recipient.resolve().setProfileAvatar(profileAvatar);
|
||||
notifyRecipientListeners();
|
||||
|
@ -15,7 +15,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
||||
|
||||
companion object {
|
||||
private const val sessionContactTable = "session_contact_database"
|
||||
const val accountID = "account_id"
|
||||
const val accountID = "session_id"
|
||||
const val name = "name"
|
||||
const val nickname = "nickname"
|
||||
const val profilePictureURL = "profile_picture_url"
|
||||
@ -42,12 +42,12 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
||||
}
|
||||
}
|
||||
|
||||
fun getContacts(sessionIDs: Collection<String>): List<Contact> {
|
||||
fun getContacts(accountIDs: Collection<String>): List<Contact> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(
|
||||
sessionContactTable,
|
||||
"$accountID IN (SELECT value FROM json_each(?))",
|
||||
arrayOf(JSONArray(sessionIDs).toString())
|
||||
arrayOf(JSONArray(accountIDs).toString())
|
||||
) { cursor -> contactFromCursor(cursor) }
|
||||
}
|
||||
|
||||
@ -56,8 +56,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
||||
return database.getAll(sessionContactTable, null, null) { cursor ->
|
||||
contactFromCursor(cursor)
|
||||
}.filter { contact ->
|
||||
val sessionId = AccountId(contact.accountID)
|
||||
sessionId.prefix == IdPrefix.STANDARD
|
||||
contact.accountID.let(::AccountId).prefix == IdPrefix.STANDARD
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,17 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import java.security.MessageDigest
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.UserProfile
|
||||
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
|
||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
||||
import network.loki.messenger.libsession_util.util.Conversation
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import network.loki.messenger.libsession_util.util.GroupInfo
|
||||
@ -94,8 +97,6 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
||||
import java.security.MessageDigest
|
||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
||||
|
||||
private const val TAG = "Storage"
|
||||
|
||||
@ -113,12 +114,12 @@ open class Storage(
|
||||
if (address.isGroup) {
|
||||
val groups = configFactory.userGroups ?: return
|
||||
if (address.isClosedGroup) {
|
||||
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||
val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||
val closedGroup = getGroup(address.toGroupString())
|
||||
if (closedGroup != null && closedGroup.isActive) {
|
||||
val legacyGroup = groups.getOrConstructLegacyGroupInfo(sessionId)
|
||||
val legacyGroup = groups.getOrConstructLegacyGroupInfo(accountId)
|
||||
groups.set(legacyGroup)
|
||||
val newVolatileParams = volatile.getOrConstructLegacyGroup(sessionId).copy(
|
||||
val newVolatileParams = volatile.getOrConstructLegacyGroup(accountId).copy(
|
||||
lastRead = SnodeAPI.nowWithOffset,
|
||||
)
|
||||
volatile.set(newVolatileParams)
|
||||
@ -134,11 +135,11 @@ open class Storage(
|
||||
if (getUserPublicKey() != address.serialize()) {
|
||||
val contacts = configFactory.contacts ?: return
|
||||
contacts.upsertContact(address.serialize()) {
|
||||
priority = ConfigBase.PRIORITY_VISIBLE
|
||||
priority = PRIORITY_VISIBLE
|
||||
}
|
||||
} else {
|
||||
val userProfile = configFactory.user ?: return
|
||||
userProfile.setNtsPriority(ConfigBase.PRIORITY_VISIBLE)
|
||||
userProfile.setNtsPriority(PRIORITY_VISIBLE)
|
||||
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true)
|
||||
}
|
||||
val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize())
|
||||
@ -151,9 +152,9 @@ open class Storage(
|
||||
if (address.isGroup) {
|
||||
val groups = configFactory.userGroups ?: return
|
||||
if (address.isClosedGroup) {
|
||||
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||
volatile.eraseLegacyClosedGroup(sessionId)
|
||||
groups.eraseLegacyGroup(sessionId)
|
||||
val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
|
||||
volatile.eraseLegacyClosedGroup(accountId)
|
||||
groups.eraseLegacyGroup(accountId)
|
||||
} else if (address.isCommunity) {
|
||||
// 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")
|
||||
@ -267,10 +268,8 @@ open class Storage(
|
||||
}
|
||||
// otherwise recipient is one to one
|
||||
recipient.isContactRecipient -> {
|
||||
// don't process non-standard session IDs though
|
||||
val sessionId = AccountId(recipient.address.serialize())
|
||||
if (sessionId.prefix != IdPrefix.STANDARD) return
|
||||
|
||||
// don't process non-standard account IDs though
|
||||
if (AccountId(recipient.address.serialize()).prefix != IdPrefix.STANDARD) return
|
||||
config.getOrConstructOneToOne(recipient.address.serialize())
|
||||
}
|
||||
else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}")
|
||||
@ -301,8 +300,8 @@ open class Storage(
|
||||
var messageID: Long? = null
|
||||
val senderAddress = fromSerialized(message.sender!!)
|
||||
val isUserSender = (message.sender!! == getUserPublicKey())
|
||||
val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let { getOpenGroup(it)?.publicKey }
|
||||
?.let { SodiumUtilities.sessionId(getUserPublicKey()!!, message.sender!!, it) } ?: false
|
||||
val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let(::getOpenGroup)?.publicKey
|
||||
?.let { SodiumUtilities.accountId(getUserPublicKey()!!, message.sender!!, it) } ?: false
|
||||
val group: Optional<SignalServiceGroup> = when {
|
||||
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
|
||||
groupPublicKey != null -> {
|
||||
@ -474,7 +473,8 @@ open class Storage(
|
||||
val userPublicKey = getUserPublicKey() ?: return
|
||||
// would love to get rid of recipient and context from this
|
||||
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
|
||||
// update name
|
||||
|
||||
// Update profile name
|
||||
val name = userProfile.getName() ?: return
|
||||
val userPic = userProfile.getPic()
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
@ -485,13 +485,14 @@ open class Storage(
|
||||
if (it != name) userProfile.setName(it)
|
||||
}
|
||||
|
||||
// update pfp
|
||||
// Update profile picture
|
||||
if (userPic == UserPic.DEFAULT) {
|
||||
clearUserPic()
|
||||
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
|
||||
&& context.prefs.getProfilePictureURL() != userPic.url) {
|
||||
setUserProfilePicture(userPic.url, userPic.key)
|
||||
}
|
||||
|
||||
if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) {
|
||||
// delete nts thread if needed
|
||||
val ourThread = getThreadId(recipient) ?: return
|
||||
@ -519,12 +520,13 @@ open class Storage(
|
||||
addLibSessionContacts(extracted, messageTimestamp)
|
||||
}
|
||||
|
||||
override fun clearUserPic() {
|
||||
val userPublicKey = getUserPublicKey() ?: return
|
||||
override fun clearUserPic() {
|
||||
val userPublicKey = getUserPublicKey() ?: return Log.w(TAG, "No user public key when trying to clear user pic")
|
||||
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
|
||||
// would love to get rid of recipient and context from this
|
||||
|
||||
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
|
||||
// clear picture if userPic is null
|
||||
|
||||
// Clear details related to the user's profile picture
|
||||
context.prefs.setProfileKey(null)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, null)
|
||||
recipientDatabase.setProfileAvatar(recipient, null)
|
||||
@ -533,14 +535,13 @@ open class Storage(
|
||||
|
||||
Recipient.removeCached(fromSerialized(userPublicKey))
|
||||
configFactory.user?.setPic(UserPic.DEFAULT)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
|
||||
private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) {
|
||||
val extracted = convos.all()
|
||||
for (conversation in extracted) {
|
||||
val threadId = when (conversation) {
|
||||
is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false)
|
||||
is Conversation.OneToOne -> getThreadIdFor(conversation.accountId, null, null, createThread = false)
|
||||
is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false)
|
||||
is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false)
|
||||
}
|
||||
@ -571,7 +572,7 @@ open class Storage(
|
||||
val existingJoinUrls = existingCommunities.values.map { it.joinURL }
|
||||
|
||||
val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup }
|
||||
val lgcIds = lgc.map { it.sessionId }
|
||||
val lgcIds = lgc.map { it.accountId }
|
||||
val toDeleteClosedGroups = existingClosedGroups.filter { group ->
|
||||
GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
|
||||
}
|
||||
@ -605,8 +606,8 @@ open class Storage(
|
||||
}
|
||||
|
||||
for (group in lgc) {
|
||||
val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId)
|
||||
val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId }
|
||||
val groupId = GroupUtil.doubleEncodeGroupID(group.accountId)
|
||||
val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.accountId }
|
||||
val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) }
|
||||
if (existingGroup != null) {
|
||||
if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
|
||||
@ -625,19 +626,19 @@ open class Storage(
|
||||
createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
|
||||
setProfileSharing(Address.fromSerialized(groupId), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
addClosedGroupPublicKey(group.sessionId)
|
||||
addClosedGroupPublicKey(group.accountId)
|
||||
// Store the encryption key pair
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
|
||||
addClosedGroupEncryptionKeyPair(keyPair, group.sessionId, SnodeAPI.nowWithOffset)
|
||||
addClosedGroupEncryptionKeyPair(keyPair, group.accountId, SnodeAPI.nowWithOffset)
|
||||
// Notify the PN server
|
||||
PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey)
|
||||
PushRegistryV1.subscribeGroup(group.accountId, publicKey = localUserPublicKey)
|
||||
// Notify the user
|
||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||
threadDb.setDate(threadID, formationTimestamp)
|
||||
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
|
||||
// Don't create config group here, it's from a config update
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(group.sessionId)
|
||||
ClosedGroupPollerV2.shared.startPolling(group.accountId)
|
||||
}
|
||||
getThreadId(Address.fromSerialized(groupId))?.let {
|
||||
setExpirationConfiguration(
|
||||
@ -938,10 +939,10 @@ open class Storage(
|
||||
groupVolatileConfig.lastRead = formationTimestamp
|
||||
volatiles.set(groupVolatileConfig)
|
||||
val groupInfo = GroupInfo.LegacyGroupInfo(
|
||||
sessionId = groupPublicKey,
|
||||
accountId = groupPublicKey,
|
||||
name = name,
|
||||
members = members,
|
||||
priority = ConfigBase.PRIORITY_VISIBLE,
|
||||
priority = PRIORITY_VISIBLE,
|
||||
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||
encSecKey = encryptionKeyPair.privateKey.serialize(),
|
||||
disappearingTimer = expirationTimer.toLong(),
|
||||
@ -975,7 +976,7 @@ open class Storage(
|
||||
members = membersMap,
|
||||
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||
encSecKey = latestKeyPair.privateKey.serialize(),
|
||||
priority = if (isPinned(threadID)) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
|
||||
priority = if (isPinned(threadID)) PRIORITY_PINNED else PRIORITY_VISIBLE,
|
||||
disappearingTimer = getExpirationConfiguration(threadID)?.expiryMode?.expirySeconds ?: 0L,
|
||||
joinedAt = (existingGroup.formationTimestamp / 1000L)
|
||||
)
|
||||
@ -1209,7 +1210,7 @@ open class Storage(
|
||||
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||
val moreContacts = contacts.filter { contact ->
|
||||
val id = AccountId(contact.id)
|
||||
id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.sessionId != null }
|
||||
id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.accountId != null }
|
||||
}
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
moreContacts.forEach { contact ->
|
||||
@ -1262,7 +1263,7 @@ open class Storage(
|
||||
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||
val moreContacts = contacts.filter { contact ->
|
||||
val id = AccountId(contact.publicKey)
|
||||
id.prefix != IdPrefix.BLINDED || mappingDb.getBlindedIdMapping(contact.publicKey).none { it.sessionId != null }
|
||||
id.prefix != IdPrefix.BLINDED || mappingDb.getBlindedIdMapping(contact.publicKey).none { it.accountId != null }
|
||||
}
|
||||
for (contact in moreContacts) {
|
||||
val address = fromSerialized(contact.publicKey)
|
||||
@ -1329,25 +1330,25 @@ open class Storage(
|
||||
val threadRecipient = getRecipientForThread(threadID) ?: return
|
||||
if (threadRecipient.isLocalNumber) {
|
||||
val user = configFactory.user ?: return
|
||||
user.setNtsPriority(if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE)
|
||||
user.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
|
||||
} else if (threadRecipient.isContactRecipient) {
|
||||
val contacts = configFactory.contacts ?: return
|
||||
contacts.upsertContact(threadRecipient.address.serialize()) {
|
||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||
priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE
|
||||
}
|
||||
} else if (threadRecipient.isGroupRecipient) {
|
||||
val groups = configFactory.userGroups ?: return
|
||||
if (threadRecipient.isClosedGroupRecipient) {
|
||||
val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize())
|
||||
val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy (
|
||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||
)
|
||||
groups.set(newGroupInfo)
|
||||
threadRecipient.address.serialize()
|
||||
.let(GroupUtil::doubleDecodeGroupId)
|
||||
.let(groups::getOrConstructLegacyGroupInfo)
|
||||
.copy (priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
|
||||
.let(groups::set)
|
||||
} else if (threadRecipient.isCommunityRecipient) {
|
||||
val openGroup = getOpenGroup(threadID) ?: return
|
||||
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
|
||||
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
|
||||
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
|
||||
priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE
|
||||
)
|
||||
groups.set(newGroupInfo)
|
||||
}
|
||||
@ -1505,10 +1506,10 @@ open class Storage(
|
||||
}
|
||||
}
|
||||
for (mapping in mappings) {
|
||||
if (!SodiumUtilities.sessionId(senderPublicKey, mapping.value.blindedId, mapping.value.serverId)) {
|
||||
if (!SodiumUtilities.accountId(senderPublicKey, mapping.value.blindedId, mapping.value.serverId)) {
|
||||
continue
|
||||
}
|
||||
mappingDb.addBlindedIdMapping(mapping.value.copy(sessionId = senderPublicKey))
|
||||
mappingDb.addBlindedIdMapping(mapping.value.copy(accountId = senderPublicKey))
|
||||
|
||||
val blindedThreadId = threadDB.getOrCreateThreadIdFor(Recipient.from(context, fromSerialized(mapping.key), false))
|
||||
mmsDb.updateThreadId(blindedThreadId, threadId)
|
||||
@ -1614,20 +1615,20 @@ open class Storage(
|
||||
): BlindedIdMapping {
|
||||
val db = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||
val mapping = db.getBlindedIdMapping(blindedId).firstOrNull() ?: BlindedIdMapping(blindedId, null, server, serverPublicKey)
|
||||
if (mapping.sessionId != null) {
|
||||
if (mapping.accountId != null) {
|
||||
return mapping
|
||||
}
|
||||
getAllContacts().forEach { contact ->
|
||||
val sessionId = AccountId(contact.accountID)
|
||||
if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString, blindedId, serverPublicKey)) {
|
||||
val contactMapping = mapping.copy(sessionId = sessionId.hexString)
|
||||
val accountId = AccountId(contact.accountID)
|
||||
if (accountId.prefix == IdPrefix.STANDARD && SodiumUtilities.accountId(accountId.hexString, blindedId, serverPublicKey)) {
|
||||
val contactMapping = mapping.copy(accountId = accountId.hexString)
|
||||
db.addBlindedIdMapping(contactMapping)
|
||||
return contactMapping
|
||||
}
|
||||
}
|
||||
db.getBlindedIdMappingsExceptFor(server).forEach {
|
||||
if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) {
|
||||
val otherMapping = mapping.copy(sessionId = it.sessionId)
|
||||
if (SodiumUtilities.accountId(it.accountId!!, blindedId, serverPublicKey)) {
|
||||
val otherMapping = mapping.copy(accountId = it.accountId)
|
||||
db.addBlindedIdMapping(otherMapping)
|
||||
return otherMapping
|
||||
}
|
||||
|
@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Moxie Marlinspike
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Represents the message record model for MMS messages that are
|
||||
* notifications (ie: they're pointers to undownloaded media).
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
private final byte[] contentLocation;
|
||||
private final long messageSize;
|
||||
private final long expiry;
|
||||
private final int status;
|
||||
private final byte[] transactionId;
|
||||
|
||||
public NotificationMmsMessageRecord(long id, Recipient conversationRecipient,
|
||||
Recipient individualRecipient,
|
||||
long dateSent, long dateReceived, int deliveryReceiptCount,
|
||||
long threadId, byte[] contentLocation, long messageSize,
|
||||
long expiry, int status, byte[] transactionId, long mailbox,
|
||||
SlideDeck slideDeck, int readReceiptCount, boolean hasMention)
|
||||
{
|
||||
super(id, "", conversationRecipient, individualRecipient,
|
||||
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
emptyList(), emptyList(),
|
||||
0, 0, slideDeck, readReceiptCount, null, emptyList(), emptyList(), false, emptyList(), hasMention);
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
this.expiry = expiry;
|
||||
this.status = status;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public byte[] getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
public int getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
public byte[] getContentLocation() {
|
||||
return contentLocation;
|
||||
}
|
||||
public long getMessageSize() {
|
||||
return (messageSize + 1023) / 1024;
|
||||
}
|
||||
public long getExpiration() {
|
||||
return expiry * 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOutgoing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPending() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMmsNotification() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMediaPending() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED) {
|
||||
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message));
|
||||
} else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) {
|
||||
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_downloading_mms_message));
|
||||
} else {
|
||||
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_error_downloading_mms_message));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Moxie Marlinspike
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
|
||||
/**
|
||||
* Represents the message record model for MMS messages that are
|
||||
* notifications (ie: they're pointers to undownloaded media).
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
class NotificationMmsMessageRecord(
|
||||
id: Long, conversationRecipient: Recipient?,
|
||||
individualRecipient: Recipient?,
|
||||
dateSent: Long,
|
||||
dateReceived: Long,
|
||||
deliveryReceiptCount: Int,
|
||||
threadId: Long,
|
||||
private val messageSize: Long,
|
||||
private val expiry: Long,
|
||||
val status: Int,
|
||||
mailbox: Long,
|
||||
slideDeck: SlideDeck?,
|
||||
readReceiptCount: Int,
|
||||
hasMention: Boolean
|
||||
) : MmsMessageRecord(
|
||||
id, "", conversationRecipient, individualRecipient,
|
||||
dateSent, dateReceived, threadId, SmsDatabase.Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
emptyList(), emptyList(),
|
||||
0, 0, slideDeck!!, readReceiptCount, null, emptyList(), emptyList(), false, emptyList(), hasMention
|
||||
) {
|
||||
fun getMessageSize(): Long {
|
||||
return (messageSize + 1023) / 1024
|
||||
}
|
||||
|
||||
val expiration: Long
|
||||
get() = expiry * 1000
|
||||
|
||||
override fun isOutgoing(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isPending(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isMmsNotification(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isMediaPending(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.GroupMemberRole
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
@ -143,9 +144,9 @@ object OpenGroupManager {
|
||||
|
||||
@WorkerThread
|
||||
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
|
||||
val url = HttpUrl.parse(urlAsString) ?: return null
|
||||
val url = urlAsString.toHttpUrlOrNull() ?: return null
|
||||
val server = OpenGroup.getServer(urlAsString)
|
||||
val room = url.pathSegments().firstOrNull() ?: return null
|
||||
val room = url.pathSegments.firstOrNull() ?: return null
|
||||
val publicKey = url.queryParameter("public_key") ?: return null
|
||||
|
||||
return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function
|
||||
|
@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
@ -22,7 +21,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.getAccentColor
|
||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||
@ -50,7 +48,7 @@ class ConversationView : LinearLayout {
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
||||
fun bind(thread: ThreadRecord, isTyping: Boolean) {
|
||||
this.thread = thread
|
||||
if (thread.isPinned) {
|
||||
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
@ -141,11 +139,10 @@ class ConversationView : LinearLayout {
|
||||
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.getSnippet(): CharSequence = listOfNotNull(
|
||||
getSnippetPrefix(),
|
||||
getDisplayBody(context)
|
||||
).joinToString(": ")
|
||||
|
||||
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
||||
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
||||
|
@ -21,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
@ -58,6 +59,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchResult
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
@ -84,6 +86,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||
|
||||
companion object {
|
||||
const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
|
||||
const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
|
||||
}
|
||||
|
||||
@ -134,9 +137,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private val isNewAccount: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
|
||||
if (!isTaskRoot) { finish(); return }
|
||||
|
||||
// Set content view
|
||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@ -171,7 +179,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
// Set up empty state view
|
||||
binding.emptyStateContainer.setThemedContent {
|
||||
EmptyView(ApplicationContext.getInstance(this).newAccount)
|
||||
EmptyView(isNewAccount)
|
||||
}
|
||||
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
@ -231,67 +239,25 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
// Get group results and display them
|
||||
launch {
|
||||
globalSearchViewModel.result.collect { result ->
|
||||
if (result.query.isEmpty()) {
|
||||
class NamedValue<T>(val name: String?, val value: T)
|
||||
|
||||
// Unknown is temporarily to be grouped together with numbers title.
|
||||
// https://optf.atlassian.net/browse/SES-2287
|
||||
val numbersTitle = "#"
|
||||
val unknownTitle = numbersTitle
|
||||
|
||||
listOf(
|
||||
GlobalSearchAdapter.Model.Header(R.string.contacts),
|
||||
GlobalSearchAdapter.Model.SavedMessages(publicKey)
|
||||
) + result.contacts
|
||||
// Remove ourself, we're shown above.
|
||||
.filter { it.accountID != publicKey }
|
||||
// Get the name that we will display and sort by, and uppercase it to
|
||||
// help with sorting and we need the char uppercased later.
|
||||
.map { (it.nickname?.takeIf(String::isNotEmpty) ?: it.name?.takeIf(String::isNotEmpty))
|
||||
.let { name -> NamedValue(name?.uppercase(), it) } }
|
||||
// Digits are all grouped under a #, the rest are grouped by their first character.uppercased()
|
||||
// If there is no name, they go under Unknown
|
||||
.groupBy { it.name?.run { first().takeUnless(Char::isDigit)?.toString() ?: numbersTitle } ?: unknownTitle }
|
||||
// place the # at the end, after all the names starting with alphabetic chars
|
||||
.toSortedMap(compareBy {
|
||||
when (it) {
|
||||
unknownTitle -> Char.MAX_VALUE
|
||||
numbersTitle -> Char.MAX_VALUE - 1
|
||||
else -> it.first()
|
||||
}
|
||||
})
|
||||
// Flatten the map of char to lists into an actual List that can be displayed.
|
||||
.flatMap { (key, contacts) ->
|
||||
listOf(
|
||||
GlobalSearchAdapter.Model.SubHeader(key)
|
||||
) + contacts.sortedBy { it.name ?: it.value.accountID }.map { it.value }.map { GlobalSearchAdapter.Model.Contact(it, it.accountID == publicKey) }
|
||||
globalSearchViewModel.result.map { result ->
|
||||
result.query to when {
|
||||
result.query.isEmpty() -> buildList {
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
|
||||
add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
|
||||
addAll(result.groupedContacts)
|
||||
}
|
||||
else -> buildList {
|
||||
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
|
||||
addAll(it)
|
||||
}
|
||||
result.messageResults.takeUnless { it.isEmpty() }?.let {
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
||||
addAll(it)
|
||||
}
|
||||
} else {
|
||||
val contactAndGroupList = result.contacts.map { GlobalSearchAdapter.Model.Contact(it, it.accountID == publicKey) } +
|
||||
result.threads.map(GlobalSearchAdapter.Model::GroupConversation)
|
||||
|
||||
val contactResults = contactAndGroupList.toMutableList()
|
||||
|
||||
if (contactResults.isNotEmpty()) {
|
||||
contactResults.add(0, GlobalSearchAdapter.Model.Header(R.string.conversations))
|
||||
}
|
||||
|
||||
val unreadThreadMap = result.messages
|
||||
.map { it.threadId }.toSet()
|
||||
.associateWith { mmsSmsDatabase.getUnreadCount(it) }
|
||||
|
||||
val messageResults: MutableList<GlobalSearchAdapter.Model> = result.messages
|
||||
.map { GlobalSearchAdapter.Model.Message(it, unreadThreadMap[it.threadId] ?: 0, it.conversationRecipient.isLocalNumber) }
|
||||
.toMutableList()
|
||||
|
||||
if (messageResults.isNotEmpty()) {
|
||||
messageResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
||||
}
|
||||
|
||||
contactResults + messageResults
|
||||
}.let { globalSearchAdapter.setNewData(result.query, it) }
|
||||
}
|
||||
}
|
||||
}.collectLatest(globalSearchAdapter::setNewData)
|
||||
}
|
||||
}
|
||||
EventBus.getDefault().register(this@HomeActivity)
|
||||
@ -309,6 +275,54 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private val GlobalSearchResult.groupedContacts: List<GlobalSearchAdapter.Model> get() {
|
||||
class NamedValue<T>(val name: String?, val value: T)
|
||||
|
||||
// Unknown is temporarily to be grouped together with numbers title.
|
||||
// https://optf.atlassian.net/browse/SES-2287
|
||||
val numbersTitle = "#"
|
||||
val unknownTitle = numbersTitle
|
||||
|
||||
return contacts
|
||||
// Remove ourself, we're shown above.
|
||||
.filter { it.accountID != publicKey }
|
||||
// Get the name that we will display and sort by, and uppercase it to
|
||||
// help with sorting and we need the char uppercased later.
|
||||
.map { (it.nickname?.takeIf(String::isNotEmpty) ?: it.name?.takeIf(String::isNotEmpty))
|
||||
.let { name -> NamedValue(name?.uppercase(), it) } }
|
||||
// Digits are all grouped under a #, the rest are grouped by their first character.uppercased()
|
||||
// If there is no name, they go under Unknown
|
||||
.groupBy { it.name?.run { first().takeUnless(Char::isDigit)?.toString() ?: numbersTitle } ?: unknownTitle }
|
||||
// place the # at the end, after all the names starting with alphabetic chars
|
||||
.toSortedMap(compareBy {
|
||||
when (it) {
|
||||
unknownTitle -> Char.MAX_VALUE
|
||||
numbersTitle -> Char.MAX_VALUE - 1
|
||||
else -> it.first()
|
||||
}
|
||||
})
|
||||
// Flatten the map of char to lists into an actual List that can be displayed.
|
||||
.flatMap { (key, contacts) ->
|
||||
listOf(
|
||||
GlobalSearchAdapter.Model.SubHeader(key)
|
||||
) + contacts.sortedBy { it.name ?: it.value.accountID }.map { it.value }.map { GlobalSearchAdapter.Model.Contact(it, it.nickname ?: it.name, it.accountID == publicKey) }
|
||||
}
|
||||
}
|
||||
|
||||
private val GlobalSearchResult.contactAndGroupList: List<GlobalSearchAdapter.Model> get() =
|
||||
contacts.map { GlobalSearchAdapter.Model.Contact(it, it.nickname ?: it.name, it.accountID == publicKey) } +
|
||||
threads.map(GlobalSearchAdapter.Model::GroupConversation)
|
||||
|
||||
private val GlobalSearchResult.messageResults: List<GlobalSearchAdapter.Model> get() {
|
||||
val unreadThreadMap = messages
|
||||
.map { it.threadId }.toSet()
|
||||
.associateWith { mmsSmsDatabase.getUnreadCount(it) }
|
||||
|
||||
return messages.map {
|
||||
GlobalSearchAdapter.Model.Message(it, unreadThreadMap[it.threadId] ?: 0, it.conversationRecipient.isLocalNumber)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInputFocusChanged(hasFocus: Boolean) {
|
||||
setSearchShown(hasFocus || binding.globalSearchInputLayout.query.value.isNotEmpty())
|
||||
}
|
||||
@ -620,9 +634,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.startHomeActivity() {
|
||||
fun Context.startHomeActivity(isNewAccount: Boolean) {
|
||||
Intent(this, HomeActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra(HomeActivity.NEW_ACCOUNT, true)
|
||||
putExtra(HomeActivity.FROM_ONBOARDING, true)
|
||||
}.also(::startActivity)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
@ -12,8 +11,6 @@ import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
|
||||
class HomeAdapter(
|
||||
private val context: Context,
|
||||
@ -115,7 +112,7 @@ class HomeAdapter(
|
||||
val offset = if (hasHeaderView()) position - 1 else position
|
||||
val thread = data.threads[offset]
|
||||
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
||||
holder.view.bind(thread, isTyping, glide)
|
||||
holder.view.bind(thread, isTyping)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||
with(binding) {
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update(recipient)
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
nameTextViewContainer.setOnClickListener {
|
||||
|
@ -28,6 +28,8 @@ class GlobalSearchAdapter(private val modelCallback: (Model)->Unit): RecyclerVie
|
||||
private var data: List<Model> = listOf()
|
||||
private var query: String? = null
|
||||
|
||||
fun setNewData(data: Pair<String, List<Model>>) = setNewData(data.first, data.second)
|
||||
|
||||
fun setNewData(query: String, newData: List<Model>) {
|
||||
val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData))
|
||||
this.query = query
|
||||
@ -134,7 +136,7 @@ class GlobalSearchAdapter(private val modelCallback: (Model)->Unit): RecyclerVie
|
||||
constructor(title: String): this(GetString(title))
|
||||
}
|
||||
data class SavedMessages(val currentUserPublicKey: String): Model()
|
||||
data class Contact(val contact: ContactModel, val isSelf: Boolean): Model()
|
||||
data class Contact(val contact: ContactModel, val name: String?, val isSelf: Boolean): Model()
|
||||
data class GroupConversation(val groupRecord: GroupRecord): Model()
|
||||
data class Message(val messageResult: MessageResult, val unread: Int, val isSelf: Boolean): Model()
|
||||
}
|
||||
|
@ -6,22 +6,18 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.session.libsignal.utilities.SettableFuture
|
||||
@ -35,16 +31,32 @@ import javax.inject.Inject
|
||||
class GlobalSearchViewModel @Inject constructor(
|
||||
private val searchRepository: SearchRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val executor = viewModelScope + SupervisorJob()
|
||||
|
||||
private val _result: MutableStateFlow<GlobalSearchResult> = MutableStateFlow(GlobalSearchResult.EMPTY)
|
||||
|
||||
val result: StateFlow<GlobalSearchResult> = _result
|
||||
|
||||
private val scope = viewModelScope + SupervisorJob()
|
||||
private val refreshes = MutableSharedFlow<Unit>()
|
||||
private val _queryText = MutableStateFlow<CharSequence>("")
|
||||
|
||||
private val _queryText: MutableStateFlow<CharSequence> = MutableStateFlow("")
|
||||
val result = _queryText
|
||||
.reEmit(refreshes)
|
||||
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.mapLatest { query ->
|
||||
if (query.trim().isEmpty()) {
|
||||
// searching for 05 as contactDb#getAllContacts was not returning contacts
|
||||
// without a nickname/name who haven't approved us.
|
||||
GlobalSearchResult(query.toString(), searchRepository.queryContacts("05").first.toList())
|
||||
} else {
|
||||
// User input delay in case we get a new query within a few hundred ms this
|
||||
// coroutine will be cancelled and the expensive query will not be run.
|
||||
delay(300)
|
||||
val settableFuture = SettableFuture<SearchResult>()
|
||||
searchRepository.query(query.toString(), settableFuture::set)
|
||||
try {
|
||||
// search repository doesn't play nicely with suspend functions (yet)
|
||||
settableFuture.get(10_000, TimeUnit.MILLISECONDS).toGlobalSearchResult()
|
||||
} catch (e: Exception) {
|
||||
GlobalSearchResult(query.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setQuery(charSequence: CharSequence) {
|
||||
_queryText.value = charSequence
|
||||
@ -55,34 +67,6 @@ class GlobalSearchViewModel @Inject constructor(
|
||||
refreshes.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
_queryText
|
||||
.reEmit(refreshes)
|
||||
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.mapLatest { query ->
|
||||
if (query.trim().isEmpty()) {
|
||||
// searching for 05 as contactDb#getAllContacts was not returning contacts
|
||||
// without a nickname/name who haven't approved us.
|
||||
GlobalSearchResult(query.toString(), searchRepository.queryContacts("05").first.toList())
|
||||
} else {
|
||||
// User input delay in case we get a new query within a few hundred ms this
|
||||
// coroutine will be cancelled and the expensive query will not be run.
|
||||
delay(300)
|
||||
val settableFuture = SettableFuture<SearchResult>()
|
||||
searchRepository.query(query.toString(), settableFuture::set)
|
||||
try {
|
||||
// search repository doesn't play nicely with suspend functions (yet)
|
||||
settableFuture.get(10_000, TimeUnit.MILLISECONDS).toGlobalSearchResult()
|
||||
} catch (e: Exception) {
|
||||
GlobalSearchResult(query.toString())
|
||||
}
|
||||
}
|
||||
}.onEach { result ->
|
||||
// update the latest _result value
|
||||
_result.value = result
|
||||
}.launchIn(executor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,6 +35,5 @@ public class AndroidLogger extends Log.Logger {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockUntilAllWritesFinished() {
|
||||
}
|
||||
public void blockUntilAllWritesFinished() { }
|
||||
}
|
||||
|
@ -34,13 +34,13 @@ class MessageRequestView : LinearLayout {
|
||||
// region Updating
|
||||
fun bind(thread: ThreadRecord, glide: GlideRequests) {
|
||||
this.thread = thread
|
||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
||||
?: thread.recipient.address.toString()
|
||||
|
||||
val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString()
|
||||
|
||||
binding.displayNameTextView.text = senderDisplayName
|
||||
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
val rawSnippet = thread.getDisplayBody(context)
|
||||
val snippet = highlightMentions(
|
||||
text = rawSnippet,
|
||||
text = thread.getDisplayBody(context),
|
||||
formatOnly = true, // no styling here, only text formatting
|
||||
threadID = thread.threadId,
|
||||
context = context
|
||||
|
@ -16,24 +16,23 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import static org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import com.squareup.phrase.Phrase;
|
||||
import java.security.SecureRandom;
|
||||
import network.loki.messenger.R;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public abstract class Slide {
|
||||
|
||||
@ -72,20 +71,23 @@ public abstract class Slide {
|
||||
return Optional.fromNullable("🎤 " + attachmentString);
|
||||
}
|
||||
}
|
||||
return Optional.fromNullable(emojiForMimeType() + attachmentString);
|
||||
String txt = Phrase.from(context, R.string.attachmentsNotification)
|
||||
.put(EMOJI_KEY, emojiForMimeType())
|
||||
.format().toString();
|
||||
return Optional.fromNullable(txt);
|
||||
}
|
||||
|
||||
private String emojiForMimeType() {
|
||||
if (MediaUtil.isImage(attachment)) {
|
||||
return "📷 ";
|
||||
return "📷";
|
||||
} else if (MediaUtil.isVideo(attachment)) {
|
||||
return "🎥 ";
|
||||
return "🎥";
|
||||
} else if (MediaUtil.isAudio(attachment)) {
|
||||
return "🎧 ";
|
||||
return "🎧";
|
||||
} else if (MediaUtil.isFile(attachment)) {
|
||||
return "📎 ";
|
||||
return "📎";
|
||||
} else {
|
||||
return "🎡 ";
|
||||
return "🎡"; // `isGif`
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,20 +157,20 @@ public abstract class Slide {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String defaultMime,
|
||||
long size,
|
||||
int width,
|
||||
int height,
|
||||
boolean hasThumbnail,
|
||||
@Nullable String fileName,
|
||||
@Nullable String caption,
|
||||
boolean voiceNote,
|
||||
boolean quote)
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String defaultMime,
|
||||
long size,
|
||||
int width,
|
||||
int height,
|
||||
boolean hasThumbnail,
|
||||
@Nullable String fileName,
|
||||
@Nullable String caption,
|
||||
boolean voiceNote,
|
||||
boolean quote)
|
||||
{
|
||||
String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime);
|
||||
String fastPreflightId = String.valueOf(new SecureRandom().nextLong());
|
||||
String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime);
|
||||
String fastPreflightId = String.valueOf(new SecureRandom().nextLong());
|
||||
return new UriAttachment(uri,
|
||||
hasThumbnail ? uri : null,
|
||||
resolvedType,
|
||||
|
@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
@ -47,8 +48,7 @@ public class SlideDeck {
|
||||
if (slide != null) slides.add(slide);
|
||||
}
|
||||
|
||||
public SlideDeck() {
|
||||
}
|
||||
public SlideDeck() { }
|
||||
|
||||
public void clear() {
|
||||
slides.clear();
|
||||
@ -65,7 +65,6 @@ public class SlideDeck {
|
||||
body = slideBody.get();
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@ -146,9 +147,8 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
}
|
||||
|
||||
public void notifyMessagesPending(Context context) {
|
||||
if (!MessagingModuleConfiguration.getShared().getPrefs().isNotificationsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MessagingModuleConfiguration.getShared().getPrefs().isNotificationsEnabled()) { return; }
|
||||
|
||||
PendingMessageNotificationBuilder builder = new PendingMessageNotificationBuilder(context, MessagingModuleConfiguration.getShared().getPrefs().getNotificationPrivacy());
|
||||
ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build());
|
||||
@ -186,9 +186,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
for (StatusBarNotification notification : activeNotifications) {
|
||||
boolean validNotification = false;
|
||||
|
||||
if (notification.getId() != SUMMARY_NOTIFICATION_ID &&
|
||||
notification.getId() != KeyCachingService.SERVICE_RUNNING_ID &&
|
||||
notification.getId() != FOREGROUND_ID &&
|
||||
if (notification.getId() != SUMMARY_NOTIFICATION_ID &&
|
||||
notification.getId() != KeyCachingService.SERVICE_RUNNING_ID &&
|
||||
notification.getId() != FOREGROUND_ID &&
|
||||
notification.getId() != PENDING_MESSAGES_ID)
|
||||
{
|
||||
for (NotificationItem item : notificationState.getNotifications()) {
|
||||
@ -198,9 +198,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
if (!validNotification) {
|
||||
notifications.cancel(notification.getId());
|
||||
}
|
||||
if (!validNotification) { notifications.cancel(notification.getId()); }
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
@ -232,7 +230,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
@Override
|
||||
public void updateNotification(@NonNull Context context, long threadId, boolean signal)
|
||||
{
|
||||
boolean isVisible = visibleThread == threadId;
|
||||
boolean isVisible = visibleThread == threadId;
|
||||
|
||||
ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase();
|
||||
Recipient recipient = threads.getRecipientForThreadId(threadId);
|
||||
@ -349,14 +347,19 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
builder.setThread(notifications.get(0).getRecipient());
|
||||
builder.setMessageCount(notificationState.getMessageCount());
|
||||
|
||||
// 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(),
|
||||
//MentionUtilities.highlightMentions(text == null ? "" : text, notifications.get(0).getThreadId(), context), // Removing hightlighting mentions -ACL
|
||||
text == null ? "" : text,
|
||||
CharSequence builderCS = text == null ? "" : text;
|
||||
SpannableString ss = MentionUtilities.highlightMentions(
|
||||
builderCS,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
bundled ? notifications.get(0).getThreadId() : 0,
|
||||
context
|
||||
);
|
||||
|
||||
builder.setPrimaryMessageBody(recipient,
|
||||
notifications.get(0).getIndividualRecipient(),
|
||||
ss,
|
||||
notifications.get(0).getSlideDeck());
|
||||
|
||||
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
|
||||
@ -506,24 +509,39 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a message request from an unknown user..
|
||||
if (messageRequest) {
|
||||
body = SpanUtil.italic(context.getString(R.string.message_requests_notification));
|
||||
|
||||
// If we received some manner of notification but Session is locked..
|
||||
} else if (KeyCachingService.isLocked(context)) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
|
||||
|
||||
// ----- All further cases assume we know the contact and that Session isn't locked -----
|
||||
|
||||
// If this is a notification about a multimedia message from a contact we know about..
|
||||
} else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
|
||||
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
|
||||
body = ContactUtil.getStringSummary(context, contact);
|
||||
|
||||
// If this is a notification about a multimedia message which contains no text but DOES contain a slide deck with at least one slide..
|
||||
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
||||
body = SpanUtil.italic(slideDeck.getBody());
|
||||
|
||||
// If this is a notification about a multimedia message, but it's not ITSELF a multimedia notification AND it contains a slide deck with at least one slide..
|
||||
} else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
||||
String message = slideDeck.getBody() + ": " + record.getBody();
|
||||
int italicLength = message.length() - body.length();
|
||||
body = SpanUtil.italic(message, italicLength);
|
||||
|
||||
// If this is a notification about an invitation to a community..
|
||||
} else if (record.isOpenGroupInvitation()) {
|
||||
body = SpanUtil.italic(context.getString(R.string.ThreadRecord_open_group_invitation));
|
||||
}
|
||||
|
||||
String userPublicKey = MessagingModuleConfiguration.getShared().getPrefs().getLocalNumber();
|
||||
String blindedPublicKey = cache.get(threadId);
|
||||
if (blindedPublicKey == null) {
|
||||
|
@ -1,11 +1,9 @@
|
||||
package org.thoughtcrime.securesms.notifications;
|
||||
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
|
||||
|
@ -10,6 +10,7 @@ import kotlinx.serialization.json.decodeFromStream
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Response
|
||||
@ -99,7 +100,7 @@ class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver)
|
||||
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
|
||||
val server = Server.LATEST
|
||||
val url = "${server.url}/$path"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), requestParameters)
|
||||
val body = RequestBody.create("application/json".toMediaType(), requestParameters)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return OnionRequestAPI.sendOnionRequest(
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.onboarding
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import network.loki.messenger.R
|
||||
@ -11,12 +12,13 @@ import org.thoughtcrime.securesms.ui.color.LocalColors
|
||||
@Composable
|
||||
fun OnboardingBackPressAlertDialog(
|
||||
dismissDialog: () -> Unit,
|
||||
@StringRes textId: Int = R.string.you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit,
|
||||
quit: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = dismissDialog,
|
||||
title = stringResource(R.string.warning),
|
||||
text = stringResource(R.string.you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit),
|
||||
text = stringResource(textId),
|
||||
buttons = listOf(
|
||||
DialogButtonModel(
|
||||
GetString(stringResource(R.string.quit)),
|
||||
|
@ -1,14 +1,10 @@
|
||||
package org.thoughtcrime.securesms.onboarding.loading
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -20,21 +16,11 @@ import org.thoughtcrime.securesms.ui.contentDescription
|
||||
import org.thoughtcrime.securesms.ui.h7
|
||||
|
||||
@Composable
|
||||
internal fun LoadingScreen(state: State) {
|
||||
val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) }
|
||||
|
||||
LaunchedEffect(state) {
|
||||
animatable.stop()
|
||||
animatable.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
internal fun LoadingScreen(progress: Float) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
ProgressArc(
|
||||
animatable.value,
|
||||
progress,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation)
|
||||
)
|
||||
Text(
|
||||
|
@ -8,7 +8,6 @@ import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.home.startHomeActivity
|
||||
@ -31,7 +30,7 @@ class LoadingActivity: BaseActionBarActivity() {
|
||||
private fun register(loadFailed: Boolean) {
|
||||
when {
|
||||
loadFailed -> startPickDisplayNameActivity(loadFailed = true)
|
||||
else -> startHomeActivity()
|
||||
else -> startHomeActivity(isNewAccount = false)
|
||||
}
|
||||
|
||||
finish()
|
||||
@ -42,11 +41,9 @@ class LoadingActivity: BaseActionBarActivity() {
|
||||
|
||||
setUpActionBarSessionLogo()
|
||||
|
||||
ApplicationContext.getInstance(this).newAccount = false
|
||||
|
||||
setComposeContent {
|
||||
val state by viewModel.states.collectAsState()
|
||||
LoadingScreen(state)
|
||||
val progress by viewModel.progress.collectAsState()
|
||||
LoadingScreen(progress)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
|
@ -4,14 +4,21 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.timeout
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -21,25 +28,43 @@ import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class State(val duration: Duration)
|
||||
enum class State {
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
FAIL
|
||||
}
|
||||
|
||||
private val ANIMATE_TO_DONE_TIME = 500.milliseconds
|
||||
private val IDLE_DONE_TIME = 1.seconds
|
||||
private val TIMEOUT_TIME = 15.seconds
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val REFRESH_TIME = 50.milliseconds
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
internal class LoadingViewModel @Inject constructor(
|
||||
val prefs: TextSecurePreferences
|
||||
): ViewModel() {
|
||||
|
||||
private val _states = MutableStateFlow(State(TIMEOUT_TIME))
|
||||
val states = _states.asStateFlow()
|
||||
private val state = MutableStateFlow(State.LOADING)
|
||||
|
||||
private val _progress = MutableStateFlow(0f)
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
private val _events = MutableSharedFlow<Event>()
|
||||
val events = _events.asSharedFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
state.flatMapLatest {
|
||||
when (it) {
|
||||
State.LOADING -> progress(0f, 1f, TIMEOUT_TIME)
|
||||
else -> progress(progress.value, 1f, ANIMATE_TO_DONE_TIME)
|
||||
}
|
||||
}.buffer(0, BufferOverflow.DROP_OLDEST)
|
||||
.collectLatest { _progress.value = it }
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
prefs.configurationMessageSyncedFlow()
|
||||
@ -54,7 +79,7 @@ internal class LoadingViewModel @Inject constructor(
|
||||
|
||||
private suspend fun onSuccess() {
|
||||
withContext(Dispatchers.Main) {
|
||||
_states.value = State(ANIMATE_TO_DONE_TIME)
|
||||
state.value = State.SUCCESS
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.SUCCESS)
|
||||
}
|
||||
@ -62,6 +87,8 @@ internal class LoadingViewModel @Inject constructor(
|
||||
|
||||
private suspend fun onFail() {
|
||||
withContext(Dispatchers.Main) {
|
||||
state.value = State.FAIL
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.TIMEOUT)
|
||||
}
|
||||
}
|
||||
@ -71,3 +98,22 @@ sealed interface Event {
|
||||
object SUCCESS: Event
|
||||
object TIMEOUT: Event
|
||||
}
|
||||
|
||||
private fun progress(
|
||||
init: Float,
|
||||
target: Float,
|
||||
time: Duration,
|
||||
refreshRate: Duration = REFRESH_TIME
|
||||
): Flow<Float> = flow {
|
||||
val startMs = System.currentTimeMillis()
|
||||
val timeMs = time.inWholeMilliseconds
|
||||
val finishMs = startMs + timeMs
|
||||
val range = target - init
|
||||
|
||||
generateSequence { System.currentTimeMillis() }.takeWhile { it < finishMs }.forEach {
|
||||
emit((it - startMs) * range / timeMs + init)
|
||||
delay(refreshRate)
|
||||
}
|
||||
|
||||
emit(target)
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ internal fun MessageNotificationsScreen(
|
||||
return
|
||||
}
|
||||
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit)
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit = quit)
|
||||
|
||||
Column {
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
@ -49,7 +49,7 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
|
||||
viewModel.events.collect {
|
||||
when (it) {
|
||||
Event.Loading -> start<LoadingActivity>()
|
||||
Event.OnboardingComplete -> startHomeActivity()
|
||||
Event.OnboardingComplete -> startHomeActivity(isNewAccount = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,11 @@ internal fun PickDisplayName(
|
||||
quit: () -> Unit = {}
|
||||
) {
|
||||
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit)
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(
|
||||
dismissDialog,
|
||||
R.string.you_cannot_go_back_further_cancel_account_creation,
|
||||
quit
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
@ -45,7 +45,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
|
||||
viewModel.events.collect {
|
||||
when (it) {
|
||||
is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName)
|
||||
Event.LoadAccountComplete -> startHomeActivity()
|
||||
Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,8 +72,6 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
|
||||
}
|
||||
|
||||
fun Context.startPickDisplayNameActivity(loadFailed: Boolean = false, flags: Int = 0) {
|
||||
ApplicationContext.getInstance(this).newAccount = !loadFailed
|
||||
|
||||
Intent(this, PickDisplayNameActivity::class.java)
|
||||
.apply { putExtra(EXTRA_LOAD_FAILED, loadFailed) }
|
||||
.also { it.flags = flags }
|
||||
|
@ -74,7 +74,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun Tabs(sessionId: String, errors: Flow<String>, onScan: (String) -> Unit) {
|
||||
private fun Tabs(accountId: String, errors: Flow<String>, onScan: (String) -> Unit) {
|
||||
val pagerState = rememberPagerState { TITLES.size }
|
||||
|
||||
Column {
|
||||
@ -84,7 +84,7 @@ private fun Tabs(sessionId: String, errors: Flow<String>, onScan: (String) -> Un
|
||||
modifier = Modifier.weight(1f)
|
||||
) { page ->
|
||||
when (TITLES[page]) {
|
||||
R.string.view -> QrPage(sessionId)
|
||||
R.string.view -> QrPage(accountId)
|
||||
R.string.scan -> MaybeScanQrCode(errors, onScan = onScan)
|
||||
}
|
||||
}
|
||||
|
@ -46,9 +46,8 @@ import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivitySettingsBinding
|
||||
import network.loki.messenger.libsession_util.util.UserPic
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.ui.alwaysUi
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
@ -87,16 +86,16 @@ import org.thoughtcrime.securesms.ui.setThemedContent
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.NetworkUtils
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "SettingsActivity"
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private val TAG = "SettingsActivity"
|
||||
|
||||
@Inject
|
||||
lateinit var configFactory: ConfigFactory
|
||||
@ -146,7 +145,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
view.apply {
|
||||
publicKey = hexEncodedPublicKey
|
||||
displayName = getDisplayName()
|
||||
isLarge = true
|
||||
update()
|
||||
}
|
||||
}
|
||||
@ -198,7 +196,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
try {
|
||||
val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
|
||||
launch(Dispatchers.Main) {
|
||||
updateProfile(true, profilePictureToBeUploaded)
|
||||
updateProfilePicture(profilePictureToBeUploaded)
|
||||
}
|
||||
} catch (e: BitmapDecodingException) {
|
||||
Log.e(TAG, e)
|
||||
@ -247,54 +245,118 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfile(
|
||||
isUpdatingProfilePicture: Boolean,
|
||||
profilePicture: ByteArray? = null,
|
||||
displayName: String? = null
|
||||
) {
|
||||
private fun updateDisplayName(displayName: String): Boolean {
|
||||
binding.loader.isVisible = true
|
||||
val promises = mutableListOf<Promise<*, Exception>>()
|
||||
if (displayName != null) {
|
||||
|
||||
// We'll assume we fail & flip the flag on success
|
||||
var updateWasSuccessful = false
|
||||
|
||||
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
|
||||
if (!haveNetworkConnection) {
|
||||
Log.w(TAG, "Cannot update display name - no network connection.")
|
||||
} else {
|
||||
// if we have a network connection then attempt to update the display name
|
||||
prefs.setProfileName(displayName)
|
||||
configFactory.user?.setName(displayName)
|
||||
}
|
||||
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
||||
if (isUpdatingProfilePicture) {
|
||||
if (profilePicture != null) {
|
||||
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
|
||||
val user = configFactory.user
|
||||
if (user == null) {
|
||||
Log.w(TAG, "Cannot update display name - missing user details from configFactory.")
|
||||
} else {
|
||||
user.setName(displayName)
|
||||
binding.btnGroupNameDisplay.text = displayName
|
||||
updateWasSuccessful = true
|
||||
}
|
||||
}
|
||||
|
||||
// Inform the user if we failed to update the display name
|
||||
if (!updateWasSuccessful) {
|
||||
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
binding.loader.isVisible = false
|
||||
return updateWasSuccessful
|
||||
}
|
||||
|
||||
// Helper method used by updateProfilePicture and removeProfilePicture to sync it online
|
||||
private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
|
||||
binding.loader.isVisible = true
|
||||
|
||||
// Grab the profile key and kick of the promise to update the profile picture
|
||||
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
||||
val updateProfilePicturePromise = ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)
|
||||
|
||||
// If the online portion of the update succeeded then update the local state
|
||||
updateProfilePicturePromise.successUi {
|
||||
|
||||
// When removing the profile picture the supplied ByteArray is empty so we'll clear the local data
|
||||
if (profilePicture.isEmpty()) {
|
||||
MessagingModuleConfiguration.shared.storage.clearUserPic()
|
||||
}
|
||||
}
|
||||
all(promises) successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
|
||||
|
||||
val userConfig = configFactory.user
|
||||
if (isUpdatingProfilePicture) {
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(prefs.getLocalNumber()!!), profilePicture)
|
||||
prefs.setProfileAvatarId(profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
// new config
|
||||
val url = prefs.getProfilePictureURL()
|
||||
val profileKey = ProfileKeyUtil.getProfileKey(this)
|
||||
if (profilePicture == null) {
|
||||
userConfig?.setPic(UserPic.DEFAULT)
|
||||
} else if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
|
||||
userConfig?.setPic(UserPic(url, profileKey))
|
||||
}
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(prefs.getLocalNumber()!!), profilePicture)
|
||||
prefs.setProfileAvatarId(SecureRandom().nextInt() )
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
|
||||
// Attempt to grab the details we require to update the profile picture
|
||||
val url = prefs.getProfilePictureURL()
|
||||
val profileKey = ProfileKeyUtil.getProfileKey(this)
|
||||
|
||||
// If we have a URL and a profile key then set the user's profile picture
|
||||
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
|
||||
userConfig?.setPic(UserPic(url, profileKey))
|
||||
}
|
||||
|
||||
if (userConfig != null && userConfig.needsDump()) {
|
||||
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
|
||||
}
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||
} alwaysUi {
|
||||
if (displayName != null) {
|
||||
binding.btnGroupNameDisplay.text = displayName
|
||||
}
|
||||
if (isUpdatingProfilePicture) {
|
||||
binding.profilePictureView.recycle() // Clear the cached image before updating
|
||||
binding.profilePictureView.update()
|
||||
}
|
||||
binding.loader.isVisible = false
|
||||
|
||||
// Update our visuals
|
||||
binding.profilePictureView.recycle()
|
||||
binding.profilePictureView.update()
|
||||
}
|
||||
|
||||
// If the sync failed then inform the user
|
||||
updateProfilePicturePromise.failUi { onFail() }
|
||||
|
||||
// Finally, remove the loader animation after we've waited for the attempt to succeed or fail
|
||||
updateProfilePicturePromise.alwaysUi { binding.loader.isVisible = false }
|
||||
}
|
||||
|
||||
private fun updateProfilePicture(profilePicture: ByteArray) {
|
||||
|
||||
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
|
||||
if (!haveNetworkConnection) {
|
||||
Log.w(TAG, "Cannot update profile picture - no network connection.")
|
||||
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val onFail: () -> Unit = {
|
||||
Log.e(TAG, "Sync failed when uploading profile picture.")
|
||||
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
syncProfilePicture(profilePicture, onFail)
|
||||
}
|
||||
|
||||
private fun removeProfilePicture() {
|
||||
|
||||
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
|
||||
if (!haveNetworkConnection) {
|
||||
Log.w(TAG, "Cannot remove profile picture - no network connection.")
|
||||
Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val onFail: () -> Unit = {
|
||||
Log.e(TAG, "Sync failed when removing profile picture.")
|
||||
Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
val emptyProfilePicture = ByteArray(0)
|
||||
syncProfilePicture(emptyProfilePicture, onFail)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -313,8 +375,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
updateProfile(false, displayName = displayName)
|
||||
return true
|
||||
return updateDisplayName(displayName)
|
||||
}
|
||||
|
||||
private fun showEditProfilePictureUI() {
|
||||
@ -323,7 +384,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
view(R.layout.dialog_change_avatar)
|
||||
button(R.string.activity_settings_upload) { startAvatarSelection() }
|
||||
if (prefs.getProfileAvatarId() != 0) {
|
||||
button(R.string.activity_settings_remove) { removeAvatar() }
|
||||
button(R.string.activity_settings_remove) { removeProfilePicture() }
|
||||
}
|
||||
cancelButton()
|
||||
}.apply {
|
||||
@ -341,10 +402,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAvatar() {
|
||||
updateProfile(true)
|
||||
}
|
||||
|
||||
private fun startAvatarSelection() {
|
||||
// Ask for an optional camera permission.
|
||||
Permissions.with(this)
|
||||
|
@ -293,7 +293,7 @@ class DefaultConversationRepository @Inject constructor(
|
||||
|
||||
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
|
||||
suspendCoroutine { continuation ->
|
||||
// Note: This sessionId could be the blinded Id
|
||||
// Note: This accountId could be the blinded Id
|
||||
val accountID = recipient.address.toString()
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||
|
||||
|
@ -81,6 +81,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
||||
const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID"
|
||||
const val EXTRA_ENABLED = "ENABLED"
|
||||
const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND"
|
||||
const val EXTRA_SWAPPED = "is_video_swapped"
|
||||
const val EXTRA_MUTE = "mute_value"
|
||||
const val EXTRA_AVAILABLE = "enabled_value"
|
||||
const val EXTRA_REMOTE_DESCRIPTION = "remote_description"
|
||||
|
@ -94,8 +94,8 @@ class ProfileManager(private val context: Context, private val configFactory: Co
|
||||
override fun contactUpdatedInternal(contact: Contact): String? {
|
||||
val contactConfig = configFactory.contacts ?: return null
|
||||
if (contact.accountID == context.prefs.getLocalNumber()) return null
|
||||
val sessionId = AccountId(contact.accountID)
|
||||
if (sessionId.prefix != IdPrefix.STANDARD) return null // only internally store standard session IDs
|
||||
val accountId = AccountId(contact.accountID)
|
||||
if (accountId.prefix != IdPrefix.STANDARD) return null // only internally store standard account IDs
|
||||
contactConfig.upsertContact(contact.accountID) {
|
||||
this.name = contact.name.orEmpty()
|
||||
this.nickname = contact.nickname.orEmpty()
|
||||
|
@ -24,6 +24,7 @@ import org.session.libsession.utilities.prefs
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
@ -31,10 +32,12 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import java.util.Timer
|
||||
|
||||
object ConfigurationMessageUtilities {
|
||||
private const val TAG = "ConfigMessageUtils"
|
||||
|
||||
private val debouncer = WindowDebouncer(3000, Timer())
|
||||
|
||||
private fun scheduleConfigSync(userPublicKey: String) {
|
||||
|
||||
debouncer.publish {
|
||||
// don't schedule job if we already have one
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
@ -44,23 +47,20 @@ object ConfigurationMessageUtilities {
|
||||
(currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true)
|
||||
return@publish
|
||||
}
|
||||
val newConfigSync = ConfigurationSyncJob(ourDestination)
|
||||
JobQueue.shared.add(newConfigSync)
|
||||
val newConfigSyncJob = ConfigurationSyncJob(ourDestination)
|
||||
JobQueue.shared.add(newConfigSyncJob)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun syncConfigurationIfNeeded(context: Context) {
|
||||
// add if check here to schedule new config job process and return early
|
||||
val userPublicKey = context.prefs.getLocalNumber() ?: return
|
||||
val userPublicKey = context.prefs.getLocalNumber() ?: return Log.w(TAG, "User Public Key is null")
|
||||
scheduleConfigSync(userPublicKey)
|
||||
}
|
||||
|
||||
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> {
|
||||
// add if check here to schedule new config job process and return early
|
||||
val userPublicKey = context.prefs.getLocalNumber() ?: return Promise.ofFail(NullPointerException("User Public Key is null"))
|
||||
// schedule job if none exist
|
||||
// don't schedule job if we already have one
|
||||
// Schedule a new job if one doesn't already exist (only)
|
||||
scheduleConfigSync(userPublicKey)
|
||||
return Promise.ofSuccess(Unit)
|
||||
}
|
||||
@ -205,7 +205,7 @@ object ConfigurationMessageUtilities {
|
||||
val admins = group.admins.map { it.serialize() to true }.toMap()
|
||||
val members = group.members.filterNot { it.serialize() !in admins.keys }.map { it.serialize() to false }.toMap()
|
||||
GroupInfo.LegacyGroupInfo(
|
||||
sessionId = groupPublicKey,
|
||||
accountId = groupPublicKey,
|
||||
name = group.title,
|
||||
members = admins + members,
|
||||
priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
|
||||
|
@ -99,7 +99,7 @@ class IP2Country private constructor(private val context: Context) {
|
||||
|
||||
val bestMatchCountry = comps.lastOrNull { it.key <= Ipv4Int(ip) }?.let { (_, code) ->
|
||||
if (code != null) {
|
||||
countryToNames[code] + " [" + ip + "]"
|
||||
countryToNames[code]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
@ -50,6 +49,17 @@ class RoundedBackgroundSpan(
|
||||
override fun getSize(
|
||||
paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?
|
||||
): Int {
|
||||
// If the span covers the whole text, and the height is not set, draw() will not be called for the span.
|
||||
// To help with that we need to take the font metric into account
|
||||
val metrics = paint.fontMetricsInt
|
||||
if (fm != null) {
|
||||
fm.top = metrics.top
|
||||
fm.ascent = metrics.ascent
|
||||
fm.descent = metrics.descent
|
||||
|
||||
fm.bottom = metrics.bottom
|
||||
}
|
||||
|
||||
return (paint.measureText(text, start, end) + 2 * paddingHorizontal).toInt()
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,14 @@ import android.telephony.TelephonyManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
@ -51,12 +54,14 @@ import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.PeerConnection.IceConnectionState
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import org.webrtc.RendererCommon
|
||||
import org.webrtc.RtpReceiver
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.ArrayDeque
|
||||
import java.util.UUID
|
||||
import kotlin.math.abs
|
||||
import org.thoughtcrime.securesms.webrtc.data.State as CallState
|
||||
|
||||
class CallManager(
|
||||
@ -105,10 +110,15 @@ class CallManager(
|
||||
|
||||
private val _audioEvents = MutableStateFlow(AudioEnabled(false))
|
||||
val audioEvents = _audioEvents.asSharedFlow()
|
||||
private val _videoEvents = MutableStateFlow(VideoEnabled(false))
|
||||
val videoEvents = _videoEvents.asSharedFlow()
|
||||
private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false))
|
||||
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow()
|
||||
|
||||
private val _videoState: MutableStateFlow<VideoState> = MutableStateFlow(
|
||||
VideoState(
|
||||
swapped = false,
|
||||
userVideoEnabled = false,
|
||||
remoteVideoEnabled = false
|
||||
)
|
||||
)
|
||||
val videoState = _videoState
|
||||
|
||||
private val stateProcessor = StateProcessor(CallState.Idle)
|
||||
|
||||
@ -151,9 +161,9 @@ class CallManager(
|
||||
|
||||
private val outgoingIceDebouncer = Debouncer(200L)
|
||||
|
||||
var localRenderer: SurfaceViewRenderer? = null
|
||||
var floatingRenderer: SurfaceViewRenderer? = null
|
||||
var remoteRotationSink: RemoteRotationVideoProxySink? = null
|
||||
var remoteRenderer: SurfaceViewRenderer? = null
|
||||
var fullscreenRenderer: SurfaceViewRenderer? = null
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
|
||||
fun clearPendingIceUpdates() {
|
||||
@ -216,20 +226,18 @@ class CallManager(
|
||||
Util.runOnMainSync {
|
||||
val base = EglBase.create()
|
||||
eglBase = base
|
||||
localRenderer = SurfaceViewRenderer(context).apply {
|
||||
// setScalingType(SCALE_ASPECT_FIT)
|
||||
}
|
||||
floatingRenderer = SurfaceViewRenderer(context)
|
||||
floatingRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
|
||||
fullscreenRenderer = SurfaceViewRenderer(context)
|
||||
fullscreenRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
|
||||
remoteRenderer = SurfaceViewRenderer(context).apply {
|
||||
// setScalingType(SCALE_ASPECT_FIT)
|
||||
}
|
||||
remoteRotationSink = RemoteRotationVideoProxySink()
|
||||
|
||||
|
||||
localRenderer?.init(base.eglBaseContext, null)
|
||||
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
|
||||
remoteRenderer?.init(base.eglBaseContext, null)
|
||||
remoteRotationSink!!.setSink(remoteRenderer!!)
|
||||
floatingRenderer?.init(base.eglBaseContext, null)
|
||||
fullscreenRenderer?.init(base.eglBaseContext, null)
|
||||
remoteRotationSink!!.setSink(fullscreenRenderer!!)
|
||||
|
||||
val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true)
|
||||
val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext)
|
||||
@ -363,7 +371,8 @@ class CallManager(
|
||||
val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] }
|
||||
val json = Json.parseToJsonElement(byteArray.decodeToString()) as JsonObject
|
||||
if (json.containsKey("video")) {
|
||||
_remoteVideoEvents.value = VideoEnabled((json["video"] as JsonPrimitive).boolean)
|
||||
_videoState.update { it.copy(remoteVideoEnabled = json["video"]?.jsonPrimitive?.boolean ?: false) }
|
||||
handleMirroring()
|
||||
} else if (json.containsKey("hangup")) {
|
||||
peerConnectionObservers.forEach(WebRtcListener::onHangup)
|
||||
}
|
||||
@ -383,13 +392,13 @@ class CallManager(
|
||||
peerConnection?.dispose()
|
||||
peerConnection = null
|
||||
|
||||
localRenderer?.release()
|
||||
floatingRenderer?.release()
|
||||
remoteRotationSink?.release()
|
||||
remoteRenderer?.release()
|
||||
fullscreenRenderer?.release()
|
||||
eglBase?.release()
|
||||
|
||||
localRenderer = null
|
||||
remoteRenderer = null
|
||||
floatingRenderer = null
|
||||
fullscreenRenderer = null
|
||||
eglBase = null
|
||||
|
||||
localCameraState = CameraState.UNKNOWN
|
||||
@ -399,8 +408,11 @@ class CallManager(
|
||||
pendingOffer = null
|
||||
callStartTime = -1
|
||||
_audioEvents.value = AudioEnabled(false)
|
||||
_videoEvents.value = VideoEnabled(false)
|
||||
_remoteVideoEvents.value = VideoEnabled(false)
|
||||
_videoState.value = VideoState(
|
||||
swapped = false,
|
||||
userVideoEnabled = false,
|
||||
remoteVideoEnabled = false
|
||||
)
|
||||
pendingOutgoingIceUpdates.clear()
|
||||
pendingIncomingIceUpdates.clear()
|
||||
}
|
||||
@ -411,7 +423,7 @@ class CallManager(
|
||||
|
||||
// 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)
|
||||
handleMirroring()
|
||||
}
|
||||
|
||||
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
|
||||
@ -469,7 +481,7 @@ class CallManager(
|
||||
val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null"))
|
||||
val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null"))
|
||||
val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
|
||||
val local = localRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
|
||||
val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
|
||||
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
|
||||
val connection = PeerConnectionWrapper(
|
||||
context,
|
||||
@ -515,7 +527,7 @@ class CallManager(
|
||||
?: return Promise.ofFail(NullPointerException("recipient is null"))
|
||||
val factory = peerConnectionFactory
|
||||
?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
|
||||
val local = localRenderer
|
||||
val local = floatingRenderer
|
||||
?: return Promise.ofFail(NullPointerException("localRenderer is null"))
|
||||
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
|
||||
|
||||
@ -609,13 +621,58 @@ class CallManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun swapVideos() {
|
||||
// update the state
|
||||
_videoState.update { it.copy(swapped = !it.swapped) }
|
||||
handleMirroring()
|
||||
|
||||
if (_videoState.value.swapped) {
|
||||
peerConnection?.rotationVideoSink?.setSink(fullscreenRenderer)
|
||||
floatingRenderer?.let{remoteRotationSink?.setSink(it) }
|
||||
} else {
|
||||
peerConnection?.rotationVideoSink?.apply {
|
||||
setSink(floatingRenderer)
|
||||
}
|
||||
fullscreenRenderer?.let{ remoteRotationSink?.setSink(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSetMuteAudio(muted: Boolean) {
|
||||
_audioEvents.value = AudioEnabled(!muted)
|
||||
peerConnection?.setAudioEnabled(!muted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the renderer currently showing the user's video, not the contact's
|
||||
*/
|
||||
private fun getUserRenderer() = if(_videoState.value.swapped) fullscreenRenderer else floatingRenderer
|
||||
|
||||
/**
|
||||
* Returns the renderer currently showing the contact's video, not the user's
|
||||
*/
|
||||
private fun getRemoteRenderer() = if(_videoState.value.swapped) floatingRenderer else fullscreenRenderer
|
||||
|
||||
/**
|
||||
* Makes sure the user's renderer applies mirroring if necessary
|
||||
*/
|
||||
private fun handleMirroring() {
|
||||
val videoState = _videoState.value
|
||||
|
||||
// if we have user video and the camera is front facing, make sure to mirror stream
|
||||
if(videoState.userVideoEnabled) {
|
||||
getUserRenderer()?.setMirror(isCameraFrontFacing())
|
||||
}
|
||||
|
||||
// the remote video is never mirrored
|
||||
if(videoState.remoteVideoEnabled){
|
||||
getRemoteRenderer()?.setMirror(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) {
|
||||
_videoEvents.value = VideoEnabled(!muted)
|
||||
_videoState.update { it.copy(userVideoEnabled = !muted) }
|
||||
handleMirroring()
|
||||
|
||||
val connection = peerConnection ?: return
|
||||
connection.setVideoEnabled(!muted)
|
||||
dataChannel?.let { channel ->
|
||||
@ -651,9 +708,18 @@ class CallManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun setDeviceRotation(newRotation: Int) {
|
||||
peerConnection?.setDeviceRotation(newRotation)
|
||||
remoteRotationSink?.rotation = newRotation
|
||||
fun setDeviceOrientation(orientation: Orientation) {
|
||||
// set rotation to the video based on the device's orientation and the camera facing direction
|
||||
val rotation = when (orientation) {
|
||||
Orientation.PORTRAIT -> 0
|
||||
Orientation.LANDSCAPE -> if (isCameraFrontFacing()) 90 else -90
|
||||
Orientation.REVERSED_LANDSCAPE -> 270
|
||||
else -> 0
|
||||
}
|
||||
|
||||
// apply the rotation to the streams
|
||||
peerConnection?.setDeviceRotation(rotation)
|
||||
remoteRotationSink?.rotation = abs(rotation) // abs as we never need the remote video to be inverted
|
||||
}
|
||||
|
||||
fun handleWiredHeadsetChanged(present: Boolean) {
|
||||
@ -721,7 +787,7 @@ class CallManager(
|
||||
connection.setCommunicationMode()
|
||||
setAudioEnabled(true)
|
||||
dataChannel?.let { channel ->
|
||||
val toSend = if (!_videoEvents.value.isEnabled) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON
|
||||
val toSend = if (_videoState.value.userVideoEnabled) VIDEO_ENABLED_JSON else VIDEO_DISABLED_JSON
|
||||
val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false)
|
||||
channel.send(buffer)
|
||||
}
|
||||
@ -750,6 +816,8 @@ class CallManager(
|
||||
|
||||
fun isInitiator(): Boolean = peerConnection?.isInitiator() == true
|
||||
|
||||
fun isCameraFrontFacing() = localCameraState.activeDirection != CameraState.Direction.BACK
|
||||
|
||||
interface WebRtcListener: PeerConnection.Observer {
|
||||
fun onHangup()
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
@ -29,16 +31,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
||||
UNTRUSTED_IDENTITY,
|
||||
}
|
||||
|
||||
val localRenderer: SurfaceViewRenderer?
|
||||
get() = callManager.localRenderer
|
||||
val floatingRenderer: SurfaceViewRenderer?
|
||||
get() = callManager.floatingRenderer
|
||||
|
||||
val remoteRenderer: SurfaceViewRenderer?
|
||||
get() = callManager.remoteRenderer
|
||||
|
||||
private var _videoEnabled: Boolean = false
|
||||
|
||||
val videoEnabled: Boolean
|
||||
get() = _videoEnabled
|
||||
val fullscreenRenderer: SurfaceViewRenderer?
|
||||
get() = callManager.fullscreenRenderer
|
||||
|
||||
private var _microphoneEnabled: Boolean = true
|
||||
|
||||
@ -59,18 +56,13 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
||||
get() = callManager.audioEvents.map { it.isEnabled }
|
||||
.onEach { _microphoneEnabled = it }
|
||||
|
||||
val localVideoEnabledState
|
||||
get() = callManager.videoEvents
|
||||
.map { it.isEnabled }
|
||||
.onEach { _videoEnabled = it }
|
||||
val videoState: StateFlow<VideoState>
|
||||
get() = callManager.videoState
|
||||
|
||||
val remoteVideoEnabledState
|
||||
get() = callManager.remoteVideoEvents.map { it.isEnabled }
|
||||
|
||||
var deviceRotation: Int = 0
|
||||
var deviceOrientation: Orientation = Orientation.UNKNOWN
|
||||
set(value) {
|
||||
field = value
|
||||
callManager.setDeviceRotation(value)
|
||||
callManager.setDeviceOrientation(value)
|
||||
}
|
||||
|
||||
val currentCallState
|
||||
@ -85,4 +77,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
||||
val callStartTime: Long
|
||||
get() = callManager.callStartTime
|
||||
|
||||
fun swapVideos() {
|
||||
callManager.swapVideos()
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
enum class Orientation {
|
||||
PORTRAIT,
|
||||
LANDSCAPE,
|
||||
REVERSED_LANDSCAPE,
|
||||
UNKNOWN
|
||||
}
|
@ -41,7 +41,7 @@ class PeerConnectionWrapper(private val context: Context,
|
||||
private val mediaStream: MediaStream
|
||||
private val videoSource: VideoSource?
|
||||
private val videoTrack: VideoTrack?
|
||||
private val rotationVideoSink = RotationVideoSink()
|
||||
public val rotationVideoSink = RotationVideoSink()
|
||||
|
||||
val readyForIce
|
||||
get() = peerConnection?.localDescription != null && peerConnection?.remoteDescription != null
|
||||
@ -103,7 +103,7 @@ class PeerConnectionWrapper(private val context: Context,
|
||||
context,
|
||||
rotationVideoSink
|
||||
)
|
||||
rotationVideoSink.mirrored = newCamera.activeDirection == CameraState.Direction.FRONT
|
||||
|
||||
rotationVideoSink.setSink(localRenderer)
|
||||
newVideoTrack.setEnabled(false)
|
||||
mediaStream.addTrack(newVideoTrack)
|
||||
|
@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
data class VideoState (
|
||||
val swapped: Boolean,
|
||||
val userVideoEnabled: Boolean,
|
||||
val remoteVideoEnabled: Boolean
|
||||
){
|
||||
fun showFloatingVideo(): Boolean {
|
||||
return userVideoEnabled && !swapped ||
|
||||
remoteVideoEnabled && swapped
|
||||
}
|
||||
|
||||
fun showFullscreenVideo(): Boolean {
|
||||
return userVideoEnabled && swapped ||
|
||||
remoteVideoEnabled && !swapped
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package org.thoughtcrime.securesms.webrtc.data
|
||||
|
||||
// get the video rotation from a specific rotation, locked into 90 degree
|
||||
// chunks offset by 45 degrees
|
||||
fun Int.quadrantRotation() = when (this % 360) {
|
||||
in 315 .. 360,
|
||||
in 0 until 45 -> 0
|
||||
in 45 until 135 -> 90
|
||||
in 135 until 225 -> 180
|
||||
else -> 270
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package org.thoughtcrime.securesms.webrtc.video
|
||||
|
||||
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
|
||||
|
||||
import org.webrtc.VideoFrame
|
||||
import org.webrtc.VideoSink
|
||||
|
||||
@ -14,8 +14,7 @@ class RemoteRotationVideoProxySink: VideoSink {
|
||||
val thisSink = targetSink ?: return
|
||||
val thisFrame = frame ?: return
|
||||
|
||||
val quadrantRotation = rotation.quadrantRotation()
|
||||
val modifiedRotation = thisFrame.rotation - quadrantRotation
|
||||
val modifiedRotation = thisFrame.rotation - rotation
|
||||
|
||||
val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs)
|
||||
thisSink.onFrame(newFrame)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.webrtc.video
|
||||
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
|
||||
|
||||
import org.webrtc.CapturerObserver
|
||||
import org.webrtc.VideoFrame
|
||||
import org.webrtc.VideoProcessor
|
||||
@ -12,7 +11,6 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
class RotationVideoSink: CapturerObserver, VideoProcessor {
|
||||
|
||||
var rotation: Int = 0
|
||||
var mirrored = false
|
||||
|
||||
private val capturing = AtomicBoolean(false)
|
||||
private var capturerObserver = SoftReference<CapturerObserver>(null)
|
||||
@ -31,13 +29,14 @@ class RotationVideoSink: CapturerObserver, VideoProcessor {
|
||||
val observer = capturerObserver.get()
|
||||
if (videoFrame == null || observer == null || !capturing.get()) return
|
||||
|
||||
val quadrantRotation = rotation.quadrantRotation()
|
||||
|
||||
val newFrame = VideoFrame(videoFrame.buffer, (videoFrame.rotation + quadrantRotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1) % 360, videoFrame.timestampNs)
|
||||
val localFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1, videoFrame.timestampNs)
|
||||
// cater for frame rotation so that the video is always facing up as we rotate pas a certain point
|
||||
val newFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation - rotation, videoFrame.timestampNs)
|
||||
|
||||
// the frame we are sending to our contact needs to cater for rotation
|
||||
observer.onFrameCaptured(newFrame)
|
||||
sink.get()?.onFrame(localFrame)
|
||||
|
||||
// the frame we see on the user's phone doesn't require changes
|
||||
sink.get()?.onFrame(videoFrame)
|
||||
}
|
||||
|
||||
override fun setSink(sink: VideoSink?) {
|
||||
|
@ -4,6 +4,6 @@
|
||||
android:color="?android:colorControlHighlight">
|
||||
|
||||
<item>
|
||||
<color android:color="?colorCellBackground" />
|
||||
<color android:color="?colorPrimary" />
|
||||
</item>
|
||||
</ripple>
|
@ -3,7 +3,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="?dialog_background_color" />
|
||||
<solid android:color="?backgroundSecondary" />
|
||||
|
||||
<corners
|
||||
android:topLeftRadius="@dimen/dialog_corner_radius"
|
||||
|
@ -3,7 +3,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="?attr/dialog_background_color" />
|
||||
<solid android:color="?backgroundSecondary" />
|
||||
|
||||
<corners android:radius="?dialogCornerRadius" />
|
||||
|
||||
|
@ -6,6 +6,6 @@
|
||||
android:insetBottom="16dp">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp" />
|
||||
<solid android:color="?dialog_background_color" />
|
||||
<solid android:color="?backgroundSecondary" />
|
||||
</shape>
|
||||
</inset>
|
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,7.59l5,-5c0.78,-0.78 2.05,-0.78 2.83,0L20.24,11h-2.83L10.4,4L5.41,9H8v2H2V5h2V7.59zM20,19h2v-6h-6v2h2.59l-4.99,5l-7.01,-7H3.76l8.41,8.41c0.78,0.78 2.05,0.78 2.83,0l5,-5V19z"/>
|
||||
</vector>
|
@ -7,5 +7,5 @@
|
||||
android:pathData="M19.907,7.674H19.907H4.54H4.54C4.317,7.674 4.095,7.719 3.888,7.806L3.888,7.806C3.681,7.893 3.491,8.023 3.334,8.189C3.176,8.355 3.054,8.554 2.978,8.775L3.922,9.097L2.978,8.775C2.903,8.996 2.877,9.231 2.904,9.465L2.904,9.465L2.904,9.469L4.555,23.412C4.555,23.413 4.555,23.413 4.555,23.414C4.603,23.823 4.807,24.189 5.111,24.447C5.415,24.705 5.798,24.84 6.187,24.84H6.188H18.26H18.26C18.649,24.84 19.032,24.705 19.336,24.447C19.64,24.189 19.844,23.823 19.892,23.414C19.892,23.413 19.892,23.413 19.892,23.412L21.543,9.469L21.544,9.465C21.57,9.231 21.544,8.996 21.469,8.775L21.469,8.775C21.393,8.554 21.271,8.355 21.113,8.189C20.956,8.023 20.766,7.893 20.559,7.806L20.17,8.728L20.559,7.806C20.352,7.719 20.13,7.674 19.907,7.674ZM21.412,1.84H3.031C2.045,1.84 1.149,2.609 1.149,3.674V5.828C1.149,6.893 2.045,7.662 3.031,7.662H21.412C22.398,7.662 23.294,6.893 23.294,5.828V3.674C23.294,2.609 22.398,1.84 21.412,1.84Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#FF3A3A"
|
||||
android:strokeColor="?colorPrimaryDark"/>
|
||||
android:strokeColor="?backgroundSecondary"/>
|
||||
</vector>
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:bottom="@dimen/small_spacing"
|
||||
>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorSettingsBackground"/>
|
||||
<solid android:color="?backgroundSecondary"/>
|
||||
<corners android:bottomLeftRadius="?preferenceCornerRadius"
|
||||
android:bottomRightRadius="?preferenceCornerRadius"/>
|
||||
</shape>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<item android:left="@dimen/medium_spacing"
|
||||
android:right="@dimen/medium_spacing">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorSettingsBackground"/>
|
||||
<solid android:color="?backgroundSecondary"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:gravity="bottom"
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:right="@dimen/medium_spacing"
|
||||
android:bottom="@dimen/small_spacing">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorSettingsBackground"/>
|
||||
<solid android:color="?backgroundSecondary"/>
|
||||
<corners android:radius="?preferenceCornerRadius"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorSettingsBackground"/>
|
||||
<solid android:color="?backgroundSecondary"/>
|
||||
<corners android:radius="?preferenceCornerRadius"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:top="@dimen/small_spacing"
|
||||
>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorSettingsBackground"/>
|
||||
<solid android:color="?backgroundSecondary"/>
|
||||
<corners android:topLeftRadius="?preferenceCornerRadius"
|
||||
android:topRightRadius="?preferenceCornerRadius"/>
|
||||
</shape>
|
||||
|
@ -1,9 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/profile_picture_background" />
|
||||
|
||||
<corners android:radius="40dp" />
|
||||
</shape>
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/profile_picture_background" />
|
||||
|
||||
<corners android:radius="23dp" />
|
||||
</shape>
|
@ -25,7 +25,7 @@
|
||||
app:cardElevation="0dp"
|
||||
app:cardCornerRadius="@dimen/dialog_corner_radius"
|
||||
android:layout_marginHorizontal="@dimen/medium_spacing"
|
||||
app:cardBackgroundColor="?colorSettingsBackground"
|
||||
app:cardBackgroundColor="?backgroundSecondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
@ -317,7 +317,7 @@
|
||||
<androidx.cardview.widget.CardView
|
||||
app:cardElevation="0dp"
|
||||
android:elevation="0dp"
|
||||
app:cardBackgroundColor="?colorSettingsBackground"
|
||||
app:cardBackgroundColor="?backgroundSecondary"
|
||||
app:cardCornerRadius="@dimen/dialog_corner_radius"
|
||||
android:layout_margin="@dimen/medium_spacing"
|
||||
android:layout_marginBottom="@dimen/massive_spacing"
|
||||
|
@ -10,7 +10,7 @@
|
||||
app:layout_constraintBottom_toTopOf="@+id/unblockButton"
|
||||
app:cardCornerRadius="?preferenceCornerRadius"
|
||||
app:cardElevation="0dp"
|
||||
app:cardBackgroundColor="?colorSettingsBackground"
|
||||
app:cardBackgroundColor="?backgroundSecondary"
|
||||
android:layout_marginHorizontal="@dimen/medium_spacing"
|
||||
android:layout_marginVertical="@dimen/large_spacing"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -8,7 +8,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/remote_parent"
|
||||
android:id="@+id/fullscreen_renderer_container"
|
||||
android:background="@color/black"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -18,20 +18,32 @@
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/remote_renderer"
|
||||
android:id="@+id/fullscreen_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
</FrameLayout>
|
||||
<ImageView
|
||||
android:id="@+id/remote_recipient"
|
||||
app:layout_constraintStart_toStartOf="@id/remote_parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/remote_parent"
|
||||
app:layout_constraintTop_toTopOf="@id/remote_parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/remote_parent"
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
android:id="@+id/userAvatar"
|
||||
app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintBottom_toBottomOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintVertical_bias="0.4"
|
||||
android:layout_width="@dimen/extra_large_profile_picture_size"
|
||||
android:layout_height="@dimen/extra_large_profile_picture_size"/>
|
||||
android:layout_height="@dimen/extra_large_profile_picture_size"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
android:id="@+id/contactAvatar"
|
||||
app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintBottom_toBottomOf="@id/fullscreen_renderer_container"
|
||||
app:layout_constraintVertical_bias="0.4"
|
||||
android:layout_width="@dimen/extra_large_profile_picture_size"
|
||||
android:layout_height="@dimen/extra_large_profile_picture_size" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/back_arrow"
|
||||
@ -71,9 +83,9 @@
|
||||
android:foregroundGravity="center"
|
||||
android:visibility="gone"
|
||||
app:SpinKit_Color="@color/core_white"
|
||||
app:layout_constraintEnd_toEndOf="@+id/remote_recipient"
|
||||
app:layout_constraintStart_toStartOf="@+id/remote_recipient"
|
||||
app:layout_constraintTop_toBottomOf="@id/remote_recipient"
|
||||
app:layout_constraintEnd_toEndOf="@+id/contactAvatar"
|
||||
app:layout_constraintStart_toStartOf="@+id/contactAvatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/contactAvatar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
@ -111,6 +123,7 @@
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/floating_renderer_container"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintDimensionRatio="h,9:16"
|
||||
@ -118,12 +131,21 @@
|
||||
android:layout_marginVertical="@dimen/massive_spacing"
|
||||
app:layout_constraintWidth_percent="0.2"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="0dp">
|
||||
android:layout_width="0dp"
|
||||
android:background="?backgroundSecondary">
|
||||
<ImageView
|
||||
android:id="@+id/videocam_off_icon"
|
||||
android:src="@drawable/ic_baseline_videocam_off_24"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
app:tint="?android:textColorPrimary"/>
|
||||
<FrameLayout
|
||||
android:elevation="8dp"
|
||||
android:id="@+id/local_renderer"
|
||||
android:id="@+id/floating_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
android:id="@+id/local_loading_view"
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
@ -133,8 +155,20 @@
|
||||
android:layout_gravity="center"
|
||||
tools:visibility="visible"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/swap_view_icon"
|
||||
android:src="@drawable/ic_baseline_screen_rotation_alt_24"
|
||||
app:layout_constraintTop_toTopOf="@id/floating_renderer_container"
|
||||
app:layout_constraintEnd_toEndOf="@id/floating_renderer_container"
|
||||
app:tint="?android:textColorPrimary"
|
||||
android:layout_marginTop="@dimen/very_small_spacing"
|
||||
android:layout_marginEnd="@dimen/very_small_spacing"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/endCallButton"
|
||||
android:background="@drawable/circle_tintable"
|
||||
|
@ -10,7 +10,7 @@
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:chipStrokeWidth="1dp"
|
||||
app:chipStrokeColor="?elementBorderColor"
|
||||
app:chipBackgroundColor="?dialog_background_color"
|
||||
app:chipBackgroundColor="?backgroundSecondary"
|
||||
app:chipMinTouchTargetSize="0dp"
|
||||
app:chipStartPadding="4dp"
|
||||
tools:ignore="TouchTargetSizeCheck"
|
||||
|
@ -85,8 +85,7 @@
|
||||
android:textColor="?unreadIndicatorTextColor"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textStyle="bold"
|
||||
tools:text="8"
|
||||
tools:textColor="?android:textColorPrimary" />
|
||||
tools:text="8"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@ -115,8 +114,7 @@
|
||||
android:textColor="?unreadIndicatorTextColor"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textStyle="bold"
|
||||
android:text="@"
|
||||
tools:textColor="?android:textColorPrimary" />
|
||||
android:text="@" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
@ -27,17 +27,11 @@
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageView
|
||||
android:scaleType="centerCrop"
|
||||
android:id="@+id/singleModeImageView"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size"
|
||||
android:background="@drawable/profile_picture_view_medium_background" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/largeSingleModeImageView"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_width="@dimen/large_profile_picture_size"
|
||||
android:layout_height="@dimen/large_profile_picture_size"
|
||||
android:background="@drawable/profile_picture_view_large_background" />
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/profile_picture_view_background" />
|
||||
|
||||
</merge>
|
@ -61,7 +61,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp"
|
||||
android:background="?colorPrimaryDark"
|
||||
android:background="?backgroundSecondary"
|
||||
app:SpinKit_Color="?android:textColorPrimary"
|
||||
android:visibility="gone"/>
|
||||
|
||||
|
@ -186,9 +186,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">أختر الكل</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">جارٍ جمع المرفقات...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">رسالة وسائط متعددة</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">تنزيل رسالة الوسائط المتعددة</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">خطأ في تنزيل رسالة الوسائط المتعددة، انقر لاعادة المحاولة</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">إرسال إلى %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -186,9 +186,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">أختر الكل</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">جارٍ جمع المرفقات...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">رسالة وسائط متعددة</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">تنزيل رسالة الوسائط المتعددة</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">خطأ في تنزيل رسالة الوسائط المتعددة، انقر لاعادة المحاولة</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">إرسال إلى %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -150,9 +150,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Hamısını seç</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Qoşmalar yığılır...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Multimedia mesajı</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">MMS mesaj endirilir</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">MMS mesajı endirmə xətası, yenidən sınamaq üçün toxunun</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">%s - göndər</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -150,9 +150,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Hamısını seç</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Qoşmalar yığılır...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Multimedia mesajı</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">MMS mesaj endirilir</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">MMS mesajı endirmə xətası, yenidən sınamaq üçün toxunun</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">%s - göndər</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -96,9 +96,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Pilih semua</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Mengumpulkan lampiran...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Pesan multimedia</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Mengunduh pesan MMS.</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Gagal saat mengunduh pesan MMS, ketuk untuk mencoba lagi</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Kirim ke %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -96,9 +96,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Pilih semua</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Mengumpulkan lampiran...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Pesan multimedia</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Mengunduh pesan MMS.</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Gagal saat mengunduh pesan MMS, ketuk untuk mencoba lagi</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Kirim ke %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -142,9 +142,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Избери всичко</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Събиране на прикачени файлове...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Мултимедийно съобщение</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Изтегляне на MMS съобщение</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Грешка при изтегляне на MMS съобщение, натиснете за да опитате повторно</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Изпрати на %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -142,9 +142,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Избери всичко</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Събиране на прикачени файлове...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Мултимедийно съобщение</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Изтегляне на MMS съобщение</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Грешка при изтегляне на MMS съобщение, натиснете за да опитате повторно</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Изпрати на %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -147,9 +147,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Selecciona-ho tot</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">S\'estan adjuntant els fitxers...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Missatge multimèdia</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">S\'està baixant el missatge MMS</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">S\'ha produït un error en baixar el missatge MMS. Toqueu per tornar a intentar-ho</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Envia-ho a %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -147,9 +147,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Selecciona-ho tot</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">S\'estan adjuntant els fitxers...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Missatge multimèdia</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">S\'està baixant el missatge MMS</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">S\'ha produït un error en baixar el missatge MMS. Toqueu per tornar a intentar-ho</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Envia-ho a %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -168,9 +168,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Označit vše</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Shromažďuji přílohy...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Multimediální zpráva</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Stahuji MMS zprávu</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Chyba při stahování MMS zprávy, ťukněte pro opakování</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Poslat %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -168,9 +168,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Označit vše</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Shromažďuji přílohy...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Multimediální zpráva</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Stahuji MMS zprávu</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Chyba při stahování MMS zprávy, ťukněte pro opakování</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Poslat %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -172,9 +172,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Dewis popeth</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Casglu atodiadau...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Neges amlgyfrwng</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Llwytho i lawr neges MMS</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Gwall wrth lawrlwytho neges MMS, tapio i geisio eto</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Anfon i %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -172,9 +172,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Dewis popeth</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Casglu atodiadau...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Neges amlgyfrwng</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Llwytho i lawr neges MMS</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Gwall wrth lawrlwytho neges MMS, tapio i geisio eto</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Anfon i %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -150,9 +150,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Vælg alle</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Samler vedhæftninger...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Multimedie besked</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Downloader MMS...</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">MMS besked kunne ikke downloades, tap for at prøve igen</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Send til %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -150,9 +150,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Vælg alle</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Samler vedhæftninger...</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">Multimedie besked</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Downloader MMS...</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">MMS besked kunne ikke downloades, tap for at prøve igen</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Send til %s</string>
|
||||
<!-- MediaSendActivity -->
|
||||
|
@ -150,9 +150,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Alle auswählen</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Anhänge werden gesammelt …</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">MMS</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">MMS wird heruntergeladen …</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Fehler beim Herunterladen der MMS. Für erneuten Versuch antippen.</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">An %s senden</string>
|
||||
<!-- MediaSendActivity -->
|
||||
@ -569,7 +566,7 @@
|
||||
<string name="activity_create_private_chat_scan_qr_code_explanation">Scannen Sie den QR-Code eines Benutzers, um eine Session zu starten. QR-Codes finden Sie, indem Sie in den Einstellungen auf das QR-Code-Symbol tippen.</string>
|
||||
<string name="fragment_enter_public_key_edit_text_hint">Sitzungs-ID oder ONS-Name eingeben</string>
|
||||
<string name="fragment_enter_public_key_explanation">Benutzer können ihre Account ID freigeben, indem sie in ihren Einstellungen auf \"Account ID freigeben\" tippen oder ihren QR-Code freigeben.</string>
|
||||
<string name="fragment_enter_public_key_error_message">Bitte überprüfe die Session-ID oder den ONS-Namen und versuche es erneut.</string>
|
||||
<string name="fragment_enter_public_key_error_message">Bitte überprüfe die Account-ID oder den ONS-Namen und versuche es erneut.</string>
|
||||
<string name="fragment_scan_qr_code_camera_access_explanation">Session benötigt Kamerazugriff, um die QR-Codes scannen zu können.</string>
|
||||
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Kamerazugriff gewähren</string>
|
||||
<string name="activity_create_closed_group_title">Neue geschlossene Gruppe</string>
|
||||
@ -641,7 +638,7 @@
|
||||
<string name="dialog_ui_mode_option_day">Tag</string>
|
||||
<string name="dialog_ui_mode_option_night">Nacht</string>
|
||||
<string name="dialog_ui_mode_option_system_default">Systemstandard</string>
|
||||
<string name="activity_conversation_menu_copy_account_id">Session-ID kopieren</string>
|
||||
<string name="activity_conversation_menu_copy_account_id">Account-ID kopieren</string>
|
||||
<string name="attachment">Anhang</string>
|
||||
<string name="attachment_type_voice_message">Sprachnachricht</string>
|
||||
<string name="details">Details</string>
|
||||
|
@ -150,9 +150,6 @@
|
||||
<string name="MediaOverviewActivity_Select_all">Alle auswählen</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Anhänge werden gesammelt …</string>
|
||||
<!-- NotificationMmsMessageRecord -->
|
||||
<string name="NotificationMmsMessageRecord_multimedia_message">MMS</string>
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">MMS wird heruntergeladen …</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Fehler beim Herunterladen der MMS. Für erneuten Versuch antippen.</string>
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">An %s senden</string>
|
||||
<!-- MediaSendActivity -->
|
||||
@ -569,7 +566,7 @@
|
||||
<string name="activity_create_private_chat_scan_qr_code_explanation">Scannen Sie den QR-Code eines Benutzers, um eine Session zu starten. QR-Codes finden Sie, indem Sie in den Einstellungen auf das QR-Code-Symbol tippen.</string>
|
||||
<string name="fragment_enter_public_key_edit_text_hint">Sitzungs-ID oder ONS-Name eingeben</string>
|
||||
<string name="fragment_enter_public_key_explanation">Benutzer können ihre Account ID freigeben, indem sie in ihren Einstellungen auf \"Account ID freigeben\" tippen oder ihren QR-Code freigeben.</string>
|
||||
<string name="fragment_enter_public_key_error_message">Bitte überprüfe die Session-ID oder den ONS-Namen und versuche es erneut.</string>
|
||||
<string name="fragment_enter_public_key_error_message">Bitte überprüfe die Account-ID oder den ONS-Namen und versuche es erneut.</string>
|
||||
<string name="fragment_scan_qr_code_camera_access_explanation">Session benötigt Kamerazugriff, um die QR-Codes scannen zu können.</string>
|
||||
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Kamerazugriff gewähren</string>
|
||||
<string name="activity_create_closed_group_title">Neue geschlossene Gruppe</string>
|
||||
@ -641,7 +638,7 @@
|
||||
<string name="dialog_ui_mode_option_day">Tag</string>
|
||||
<string name="dialog_ui_mode_option_night">Nacht</string>
|
||||
<string name="dialog_ui_mode_option_system_default">Systemstandard</string>
|
||||
<string name="activity_conversation_menu_copy_account_id">Session-ID kopieren</string>
|
||||
<string name="activity_conversation_menu_copy_account_id">Account-ID kopieren</string>
|
||||
<string name="attachment">Anhang</string>
|
||||
<string name="attachment_type_voice_message">Sprachnachricht</string>
|
||||
<string name="details">Details</string>
|
||||
|
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