Merge branch 'dev' into just-prefs

This commit is contained in:
bemusementpark 2024-07-18 12:39:49 +09:30
commit 268644edd2
240 changed files with 495468 additions and 297714 deletions

View File

@ -31,8 +31,8 @@ configurations.all {
exclude module: "commons-logging" exclude module: "commons-logging"
} }
def canonicalVersionCode = 373 def canonicalVersionCode = 374
def canonicalVersionName = "1.18.4" def canonicalVersionName = "1.18.5"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -271,7 +271,7 @@ dependencies {
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300' 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-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui: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.signal:aesgcmprovider:0.0.3'
implementation 'org.webrtc:google-webrtc:1.0.32006' implementation 'org.webrtc:google-webrtc:1.0.32006'
implementation "me.leolin:ShortcutBadger:1.1.16" implementation "me.leolin:ShortcutBadger:1.1.16"
@ -322,6 +322,7 @@ dependencies {
implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.phrase:phrase:$phraseVersion"
implementation 'app.cash.copper:copper-flow:1.0.0' implementation 'app.cash.copper:copper-flow:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"

View File

@ -136,29 +136,29 @@ class SodiumUtilitiesTest {
} }
@Test @Test
fun sessionIdSuccess() { fun accountIdSuccess() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", serverPublicKey) val result = SodiumUtilities.accountId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
assertTrue(result) assertTrue(result)
} }
@Test @Test
fun sessionIdFailureInvalidAccountId() { fun accountIdFailureInvalidAccountId() {
val result = SodiumUtilities.sessionId("AB$publicKey", "15$blindedPublicKey", serverPublicKey) val result = SodiumUtilities.accountId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
assertFalse(result) assertFalse(result)
} }
@Test @Test
fun sessionIdFailureInvalidBlindedId() { fun accountIdFailureInvalidBlindedId() {
val result = SodiumUtilities.sessionId("05$publicKey", "AB$blindedPublicKey", serverPublicKey) val result = SodiumUtilities.accountId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
assertFalse(result) assertFalse(result)
} }
@Test @Test
fun sessionIdFailureBlindingFactor() { fun accountIdFailureBlindingFactor() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", "Test") val result = SodiumUtilities.accountId("05$publicKey", "15$blindedPublicKey", "Test")
assertFalse(result) assertFalse(result)
} }

View File

@ -125,7 +125,7 @@
<activity <activity
android:name="org.thoughtcrime.securesms.home.HomeActivity" android:name="org.thoughtcrime.securesms.home.HomeActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:launchMode="singleTask" android:launchMode="standard"
android:theme="@style/Theme.Session.DayNight.NoActionBar" /> android:theme="@style/Theme.Session.DayNight.NoActionBar" />
<activity <activity
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity" android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_na
226074,en,AF,Africa,UG,Uganda,0 226074,en,AF,Africa,UG,Uganda,0
239880,en,AF,Africa,CF,"Central African Republic",0 239880,en,AF,Africa,CF,"Central African Republic",0
241170,en,AF,Africa,SC,Seychelles,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 272103,en,AS,Asia,LB,Lebanon,0
285570,en,AS,Asia,KW,Kuwait,0 285570,en,AS,Asia,KW,Kuwait,0
286963,en,AS,Asia,OM,Oman,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 290291,en,AS,Asia,BH,Bahrain,0
290557,en,AS,Asia,AE,"United Arab Emirates",0 290557,en,AS,Asia,AE,"United Arab Emirates",0
294640,en,AS,Asia,IL,Israel,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 337996,en,AF,Africa,ET,Ethiopia,0
338010,en,AF,Africa,ER,Eritrea,0 338010,en,AF,Africa,ER,Eritrea,0
357994,en,AF,Africa,EG,Egypt,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 453733,en,EU,Europe,EE,Estonia,1
458258,en,EU,Europe,LV,Latvia,1 458258,en,EU,Europe,LV,Latvia,1
587116,en,AS,Asia,AZ,Azerbaijan,0 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 607072,en,EU,Europe,SJ,"Svalbard and Jan Mayen",0
614540,en,AS,Asia,GE,Georgia,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 630336,en,EU,Europe,BY,Belarus,0
660013,en,EU,Europe,FI,Finland,1 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 690791,en,EU,Europe,UA,Ukraine,0
718075,en,EU,Europe,MK,"North Macedonia",0 718075,en,EU,Europe,MK,"North Macedonia",0
719819,en,EU,Europe,HU,Hungary,1 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 1522867,en,AS,Asia,KZ,Kazakhstan,0
1527747,en,AS,Asia,KG,Kyrgyzstan,0 1527747,en,AS,Asia,KG,Kyrgyzstan,0
1546748,en,AN,Antarctica,TF,"French Southern Territories",0 1546748,en,AN,Antarctica,TF,"French Southern Territories",0
1547314,en,AN,Antarctica,HM,"Heard Island and McDonald Islands",0 1547314,en,AN,Antarctica,HM,"Heard and McDonald Islands",0
1547376,en,AS,Asia,CC,"Cocos [Keeling] Islands",0 1547376,en,AS,Asia,CC,"Cocos (Keeling) Islands",0
1559582,en,OC,Oceania,PW,Palau,0 1559582,en,OC,Oceania,PW,Palau,0
1562822,en,AS,Asia,VN,Vietnam,0 1562822,en,AS,Asia,VN,Vietnam,0
1605651,en,AS,Asia,TH,Thailand,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 1873107,en,AS,Asia,KP,"North Korea",0
1880251,en,AS,Asia,SG,Singapore,0 1880251,en,AS,Asia,SG,Singapore,0
1899402,en,OC,Oceania,CK,"Cook Islands",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 2017370,en,EU,Europe,RU,Russia,0
2029969,en,AS,Asia,MN,Mongolia,0 2029969,en,AS,Asia,MN,Mongolia,0
2077456,en,OC,Oceania,AU,Australia,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 2400553,en,AF,Africa,GA,Gabon,0
2403846,en,AF,Africa,SL,"Sierra Leone",0 2403846,en,AF,Africa,SL,"Sierra Leone",0
2410758,en,AF,Africa,ST,"São Tomé and Príncipe",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 2413451,en,AF,Africa,GM,Gambia,0
2420477,en,AF,Africa,GN,Guinea,0 2420477,en,AF,Africa,GN,Guinea,0
2434508,en,AF,Africa,TD,Chad,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 2622320,en,EU,Europe,FO,"Faroe Islands",0
2623032,en,EU,Europe,DK,Denmark,1 2623032,en,EU,Europe,DK,Denmark,1
2629691,en,EU,Europe,IS,Iceland,0 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 2658434,en,EU,Europe,CH,Switzerland,0
2661886,en,EU,Europe,SE,Sweden,1 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 2782113,en,EU,Europe,AT,Austria,1
2802361,en,EU,Europe,BE,Belgium,1 2802361,en,EU,Europe,BE,Belgium,1
2921044,en,EU,Europe,DE,Germany,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 3576916,en,NA,"North America",TC,"Turks and Caicos Islands",0
3577279,en,NA,"North America",AW,Aruba,0 3577279,en,NA,"North America",AW,Aruba,0
3577718,en,NA,"North America",VG,"British Virgin Islands",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 3578097,en,NA,"North America",MS,Montserrat,0
3578421,en,NA,"North America",MF,"Saint Martin",1 3578421,en,NA,"North America",MF,"Saint Martin",1
3578476,en,NA,"North America",BL,"Saint Barthélemy",0 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 4043988,en,OC,Oceania,GU,Guam,0
4566966,en,NA,"North America",PR,"Puerto Rico",0 4566966,en,NA,"North America",PR,"Puerto Rico",0
4796775,en,NA,"North America",VI,"U.S. Virgin Islands",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 5880801,en,OC,Oceania,AS,"American Samoa",0
6251999,en,NA,"North America",CA,Canada,0 6251999,en,NA,"North America",CA,Canada,0
6252001,en,NA,"North America",US,"United States",0 6252001,en,NA,"North America",US,"United States",0

1 geoname_id locale_code continent_code continent_name country_iso_code country_name is_in_european_union
15 226074 en AF Africa UG Uganda 0
16 239880 en AF Africa CF Central African Republic 0
17 241170 en AF Africa SC Seychelles 0
18 248816 en AS Asia JO Hashemite Kingdom of Jordan Jordan 0
19 272103 en AS Asia LB Lebanon 0
20 285570 en AS Asia KW Kuwait 0
21 286963 en AS Asia OM Oman 0
23 290291 en AS Asia BH Bahrain 0
24 290557 en AS Asia AE United Arab Emirates 0
25 294640 en AS Asia IL Israel 0
26 298795 en AS Asia TR Turkey Türkiye 0
27 337996 en AF Africa ET Ethiopia 0
28 338010 en AF Africa ER Eritrea 0
29 357994 en AF Africa EG Egypt 0
33 453733 en EU Europe EE Estonia 1
34 458258 en EU Europe LV Latvia 1
35 587116 en AS Asia AZ Azerbaijan 0
36 597427 en EU Europe LT Republic of Lithuania Lithuania 1
37 607072 en EU Europe SJ Svalbard and Jan Mayen 0
38 614540 en AS Asia GE Georgia 0
39 617790 en EU Europe MD Republic of Moldova Moldova 0
40 630336 en EU Europe BY Belarus 0
41 660013 en EU Europe FI Finland 1
42 661882 en EU Europe AX Åland Åland Islands 1
43 690791 en EU Europe UA Ukraine 0
44 718075 en EU Europe MK North Macedonia 0
45 719819 en EU Europe HU Hungary 1
77 1522867 en AS Asia KZ Kazakhstan 0
78 1527747 en AS Asia KG Kyrgyzstan 0
79 1546748 en AN Antarctica TF French Southern Territories 0
80 1547314 en AN Antarctica HM Heard Island and McDonald Islands Heard and McDonald Islands 0
81 1547376 en AS Asia CC Cocos [Keeling] Islands Cocos (Keeling) Islands 0
82 1559582 en OC Oceania PW Palau 0
83 1562822 en AS Asia VN Vietnam 0
84 1605651 en AS Asia TH Thailand 0
97 1873107 en AS Asia KP North Korea 0
98 1880251 en AS Asia SG Singapore 0
99 1899402 en OC Oceania CK Cook Islands 0
100 1966436 en OC Oceania TL East Timor Timor-Leste 0
101 2017370 en EU Europe RU Russia 0
102 2029969 en AS Asia MN Mongolia 0
103 2077456 en OC Oceania AU Australia 0
131 2400553 en AF Africa GA Gabon 0
132 2403846 en AF Africa SL Sierra Leone 0
133 2410758 en AF Africa ST São Tomé and Príncipe 0
134 2411586 en EU Europe GI Gibraltar 1 0
135 2413451 en AF Africa GM Gambia 0
136 2420477 en AF Africa GN Guinea 0
137 2434508 en AF Africa TD Chad 0
146 2622320 en EU Europe FO Faroe Islands 0
147 2623032 en EU Europe DK Denmark 1
148 2629691 en EU Europe IS Iceland 0
149 2635167 en EU Europe GB United Kingdom 1 0
150 2658434 en EU Europe CH Switzerland 0
151 2661886 en EU Europe SE Sweden 1
152 2750405 en EU Europe NL Netherlands The Netherlands 1
153 2782113 en EU Europe AT Austria 1
154 2802361 en EU Europe BE Belgium 1
155 2921044 en EU Europe DE Germany 1
203 3576916 en NA North America TC Turks and Caicos Islands 0
204 3577279 en NA North America AW Aruba 0
205 3577718 en NA North America VG British Virgin Islands 0
206 3577815 en NA North America VC Saint Vincent and the Grenadines St Vincent and Grenadines 0
207 3578097 en NA North America MS Montserrat 0
208 3578421 en NA North America MF Saint Martin 1
209 3578476 en NA North America BL Saint Barthélemy 0
238 4043988 en OC Oceania GU Guam 0
239 4566966 en NA North America PR Puerto Rico 0
240 4796775 en NA North America VI U.S. Virgin Islands 0
241 5854968 en OC Oceania UM U.S. Minor Outlying Islands U.S. Outlying Islands 0
242 5880801 en OC Oceania AS American Samoa 0
243 6251999 en NA North America CA Canada 0
244 6252001 en NA North America US United States 0

View File

@ -154,8 +154,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private volatile boolean isAppVisible; private volatile boolean isAppVisible;
public boolean newAccount = false;
public static ApplicationContext getInstance(Context context) { public static ApplicationContext getInstance(Context context) {
return (ApplicationContext) context.getApplicationContext(); return (ApplicationContext) context.getApplicationContext();
} }

View File

@ -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) {}
}

View File

@ -5,11 +5,17 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter 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.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem import android.view.MenuItem
import android.view.OrientationEventListener import android.view.View
import android.view.ViewOutlineProvider
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -21,13 +27,14 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import android.provider.Settings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityWebrtcBinding import network.loki.messenger.databinding.ActivityWebrtcBinding
import org.apache.commons.lang3.time.DurationFormatUtils import org.apache.commons.lang3.time.DurationFormatUtils
import org.session.libsession.avatars.ProfileContactPhoto import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.contacts.Contact 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.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent 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_PRE_INIT
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING 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.EARPIECE
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE
import javax.inject.Inject
import kotlin.math.asin
@AndroidEntryPoint @AndroidEntryPoint
class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
@ -60,6 +70,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
private const val CALL_DURATION_FORMAT = "HH:mm:ss" private const val CALL_DURATION_FORMAT = "HH:mm:ss"
} }
@Inject lateinit var prefs: TextSecurePreferences
private val viewModel by viewModels<CallViewModel>() private val viewModel by viewModels<CallViewModel>()
private val glide by lazy { GlideApp.with(this) } private val glide by lazy { GlideApp.with(this) }
private lateinit var binding: ActivityWebrtcBinding private lateinit var binding: ActivityWebrtcBinding
@ -71,16 +83,13 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
} }
private var hangupReceiver: BroadcastReceiver? = null private var hangupReceiver: BroadcastReceiver? = null
private val rotationListener by lazy { /**
object : OrientationEventListener(this) { * We need to track the device's orientation so we can calculate whether or not to rotate the video streams
override fun onOrientationChanged(orientation: Int) { * This works a lot better than using `OrientationEventListener > onOrientationChanged'
if ((orientation + 15) % 90 < 30) { * which gives us a rotation angle that doesn't take into account pitch vs roll, so tipping the device from front to back would
viewModel.deviceRotation = orientation * trigger the video rotation logic, while we really only want it when the device is in portrait or landscape.
// updateControlsRotation(orientation.quadrantRotation() * -1) */
} private var orientationManager = OrientationManager(this)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) { if (item.itemId == android.R.id.home) {
@ -102,13 +111,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready) 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) binding = ActivityWebrtcBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@ -136,6 +138,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(false)
} }
binding.floatingRendererContainer.setOnClickListener {
viewModel.swapVideos()
}
binding.microphoneButton.setOnClickListener { binding.microphoneButton.setOnClickListener {
val audioEnabledIntent = val audioEnabledIntent =
WebRtcCallService.microphoneIntent(this, !viewModel.microphoneEnabled) WebRtcCallService.microphoneIntent(this, !viewModel.microphoneEnabled)
@ -174,7 +180,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.onAllGranted { .onAllGranted {
val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoEnabled) val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoState.value.userVideoEnabled)
startService(intent) startService(intent)
} }
.execute() .execute()
@ -191,14 +197,53 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
onBackPressed() 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 { * Makes sure the floating video inset has clipped rounded corners, included with the video stream itself
return Settings.System.getInt( */
contentResolver, private fun clipFloatingInsets() {
Settings.System.ACCELEROMETER_ROTATION, 0 // clip the video inset with rounded corners
) == 1 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() { override fun onDestroy() {
@ -206,7 +251,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
hangupReceiver?.let { receiver -> hangupReceiver?.let { receiver ->
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
} }
rotationListener.disable()
orientationManager.destroy()
} }
private fun answerCall() { private fun answerCall() {
@ -214,15 +260,33 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
ContextCompat.startForegroundService(this, answerIntent) ContextCompat.startForegroundService(this, answerIntent)
} }
private fun updateControlsRotation(newRotation: Int) { private fun updateControlsRotation() {
with (binding) { with (binding) {
val rotation = newRotation.toFloat() val rotation = when(viewModel.deviceOrientation){
remoteRecipient.rotation = rotation Orientation.LANDSCAPE -> -90f
speakerPhoneButton.rotation = rotation Orientation.REVERSED_LANDSCAPE -> 90f
microphoneButton.rotation = rotation else -> 0f
enableCameraButton.rotation = rotation }
switchCameraButton.rotation = rotation
endCallButton.rotation = rotation 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 { launch {
viewModel.recipient.collect { latestRecipient -> viewModel.recipient.collect { latestRecipient ->
binding.contactAvatar.recycle()
if (latestRecipient.recipient != null) { if (latestRecipient.recipient != null) {
val publicKey = latestRecipient.recipient.address.serialize() val contactPublicKey = latestRecipient.recipient.address.serialize()
val displayName = getUserDisplayName(publicKey) val contactDisplayName = getUserDisplayName(contactPublicKey)
supportActionBar?.title = displayName supportActionBar?.title = contactDisplayName
val signalProfilePicture = latestRecipient.recipient.contactPhoto binding.remoteRecipientName.text = contactDisplayName
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
val sizeInPX = // sort out the contact's avatar
resources.getDimensionPixelSize(R.dimen.extra_large_profile_picture_size) binding.contactAvatar.apply {
binding.remoteRecipientName.text = displayName publicKey = contactPublicKey
if (signalProfilePicture != null && avatar != "0" && avatar != "") { displayName = contactDisplayName
glide.clear(binding.remoteRecipient) update()
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)
} }
} else {
glide.clear(binding.remoteRecipient)
} }
} }
} }
@ -346,39 +386,65 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
} }
} }
// handle video state
launch { launch {
viewModel.localVideoEnabledState.collect { isEnabled -> viewModel.videoState.collect { state ->
binding.localRenderer.removeAllViews() binding.floatingRenderer.removeAllViews()
if (isEnabled) { binding.fullscreenRenderer.removeAllViews()
viewModel.localRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
// Mirror the video preview of the person making the call to prevent disorienting them // handle fullscreen video window
surfaceView.setMirror(true) if(state.showFullscreenVideo()){
viewModel.fullscreenRenderer?.let { surfaceView ->
binding.localRenderer.addView(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 { // handle floating video window
viewModel.remoteVideoEnabledState.collect { isEnabled -> if(state.showFloatingVideo()){
binding.remoteRenderer.removeAllViews() viewModel.floatingRenderer?.let { surfaceView ->
if (isEnabled) { binding.floatingRenderer.addView(surfaceView)
viewModel.remoteRenderer?.let { surfaceView -> binding.floatingRenderer.isVisible = true
binding.remoteRenderer.addView(surfaceView) 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 { private fun getUserDisplayName(publicKey: String): String {
val contact = val contact =
DatabaseComponent.get(this).sessionContactDatabase().getContactWithAccountID(publicKey) DatabaseComponent.get(this).sessionContactDatabase().getContactWithAccountID(publicKey)
@ -388,7 +454,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
uiJob?.cancel() uiJob?.cancel()
binding.remoteRenderer.removeAllViews() binding.fullscreenRenderer.removeAllViews()
binding.localRenderer.removeAllViews() binding.floatingRenderer.removeAllViews()
} }
} }

View File

@ -18,6 +18,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
@ -25,6 +26,8 @@ import org.thoughtcrime.securesms.mms.GlideRequests
class ProfilePictureView @JvmOverloads constructor( class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) { ) : RelativeLayout(context, attrs) {
private val TAG = "ProfilePictureView"
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this) private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
private val glide: GlideRequests = GlideApp.with(this) private val glide: GlideRequests = GlideApp.with(this)
private val prefs = TextSecurePreferences(context) private val prefs = TextSecurePreferences(context)
@ -33,7 +36,6 @@ class ProfilePictureView @JvmOverloads constructor(
var displayName: String? = null var displayName: String? = null
var additionalPublicKey: String? = null var additionalPublicKey: String? = null
var additionalDisplayName: String? = null var additionalDisplayName: String? = null
var isLarge = false
private val profilePicturesCache = mutableMapOf<View, Recipient>() private val profilePicturesCache = mutableMapOf<View, Recipient>()
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default) private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
@ -91,31 +93,27 @@ class ProfilePictureView @JvmOverloads constructor(
} }
fun update() { 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 val additionalPublicKey = additionalPublicKey
// if we have a multi avatar setup
if (additionalPublicKey != null) { if (additionalPublicKey != null) {
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName) setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName) setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
binding.doubleModeImageViewContainer.visibility = View.VISIBLE 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.doubleModeImageView1)
glide.clear(binding.doubleModeImageView2) glide.clear(binding.doubleModeImageView2)
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE 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?) { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {

View File

@ -49,17 +49,20 @@ internal fun StartConversationScreen(
ItemButton( ItemButton(
textId = R.string.messageNew, textId = R.string.messageNew,
icon = R.drawable.ic_message, icon = R.drawable.ic_message,
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
onClick = delegate::onNewMessageSelected) onClick = delegate::onNewMessageSelected)
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.activity_create_group_title, textId = R.string.activity_create_group_title,
icon = R.drawable.ic_group, icon = R.drawable.ic_group,
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
onClick = delegate::onCreateGroupSelected onClick = delegate::onCreateGroupSelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.dialog_join_community_title, textId = R.string.dialog_join_community_title,
icon = R.drawable.ic_globe, icon = R.drawable.ic_globe,
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
onClick = delegate::onJoinCommunitySelected onClick = delegate::onJoinCommunitySelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)

View File

@ -85,8 +85,8 @@ private fun EnterAccountId(
SessionOutlinedTextField( SessionOutlinedTextField(
text = state.newMessageIdOrOns, text = state.newMessageIdOrOns,
modifier = Modifier modifier = Modifier
.padding(horizontal = LocalDimensions.current.smallMargin) .padding(horizontal = LocalDimensions.current.smallMargin),
.contentDescription("Session id input box"), contentDescription = "Session id input box",
placeholder = stringResource(R.string.accountIdOrOnsEnter), placeholder = stringResource(R.string.accountIdOrOnsEnter),
onChange = callbacks::onChange, onChange = callbacks::onChange,
onContinue = callbacks::onContinue, onContinue = callbacks::onContinue,

View File

@ -45,7 +45,7 @@ class NewMessageFragment : Fragment() {
viewModel, viewModel,
onClose = { delegate.onDialogClosePressed() }, onClose = { delegate.onDialogClosePressed() },
onBack = { delegate.onDialogBackPressed() }, 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") }
) )
} }

View File

@ -239,12 +239,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
intent.getParcelableExtra<Address>(ADDRESS)?.let { it -> intent.getParcelableExtra<Address>(ADDRESS)?.let { it ->
threadId = threadDb.getThreadIdIfExistsFor(it.serialize()) threadId = threadDb.getThreadIdIfExistsFor(it.serialize())
if (threadId == -1L) { 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 openGroup = lokiThreadDb.getOpenGroupChat(intent.getLongExtra(FROM_GROUP_THREAD_ID, -1))
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) { val address = if (accountId.prefix == IdPrefix.BLINDED && openGroup != null) {
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let { storage.getOrCreateBlindedIdMapping(accountId.hexString, openGroup.server, openGroup.publicKey).accountId?.let {
fromSerialized(it) fromSerialized(it)
} ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId) } ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, accountId)
} else { } else {
it it
} }
@ -1131,8 +1131,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
override fun copyAccountID(sessionId: String) { override fun copyAccountID(accountId: String) {
val clip = ClipData.newPlainText("Account ID", sessionId) val clip = ClipData.newPlainText("Account ID", accountId)
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()

View File

@ -2,19 +2,16 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.graphics.PointF import android.graphics.PointF
import android.net.Uri import android.net.Uri
import android.text.Editable import android.text.Editable
import android.text.InputType import android.text.InputType
import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isGone 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.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SimpleTextWatcher
import org.thoughtcrime.securesms.util.addTextChangedListener import org.thoughtcrime.securesms.util.addTextChangedListener
import org.thoughtcrime.securesms.util.contains 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 // 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. // manipulate the UI faster than we can setup & teardown.
@ -48,16 +42,24 @@ enum class VoiceRecorderState {
ShuttingDownAfterRecord 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 { TextView.OnEditorActionListener {
private lateinit var binding: ViewInputBarBinding
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private var binding: ViewInputBarBinding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
private val vMargin by lazy { toDp(4, resources) }
private val minHeight by lazy { toPx(56, resources) }
private var linkPreviewDraftView: LinkPreviewDraftView? = null private var linkPreviewDraftView: LinkPreviewDraftView? = null
private var quoteView: QuoteView? = null private var quoteView: QuoteView? = null
var delegate: InputBarDelegate? = null var delegate: InputBarDelegate? = null
var additionalContentHeight = 0
var quote: MessageRecord? = null var quote: MessageRecord? = null
var linkPreview: LinkPreview? = null var linkPreview: LinkPreview? = null
var showInput: Boolean = true var showInput: Boolean = true
@ -70,7 +72,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
var text: String var text: String
get() { return binding.inputBarEditText.text?.toString() ?: "" } get() = binding.inputBarEditText.text?.toString() ?: ""
set(value) { binding.inputBarEditText.setText(value) } set(value) { binding.inputBarEditText.setText(value) }
// Keep track of when the user pressed the record voice message button, the duration that // 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 voiceMessageDurationMS = 0L
var voiceRecorderState = VoiceRecorderState.Idle var voiceRecorderState = VoiceRecorderState.Idle
val attachmentButtonsContainerHeight: Int private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)}
get() = binding.attachmentsButtonContainer.height 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)} } init {
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)
// Attachments button // Attachments button
binding.attachmentsButtonContainer.addView(attachmentsButton) binding.attachmentsButtonContainer.addView(attachmentsButton)
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) 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.onUp` and tap the button then the logged output order is onUp and THEN onPress!
microphoneButton.setOnTouchListener(object : OnTouchListener { microphoneButton.setOnTouchListener(object : OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean { 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 // We only handle single finger touch events so just consume the event and bail if there are more
if (event.pointerCount > 1) return true if (event.pointerCount > 1) return true
@ -162,12 +155,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
binding.inputBarEditText.setOnEditorActionListener(this) binding.inputBarEditText.setOnEditorActionListener(this)
if (context.prefs.isEnterSendsEnabled()) { if (context.prefs.isEnterSendsEnabled()) {
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_SEND binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_SEND
binding.inputBarEditText.inputType = binding.inputBarEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
} else { } else {
binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_NONE binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_NONE
binding.inputBarEditText.inputType = binding.inputBarEditText.inputType =
binding.inputBarEditText.inputType or binding.inputBarEditText.inputType
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
} }
val incognitoFlag = if (context.prefs.isIncognitoKeyboardEnabled()) 16777216 else 0 val incognitoFlag = if (context.prefs.isIncognitoKeyboardEnabled()) 16777216 else 0
@ -184,9 +176,6 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
return false return false
} }
// endregion
// region Updating
override fun inputBarEditTextContentChanged(text: CharSequence) { override fun inputBarEditTextContentChanged(text: CharSequence) {
microphoneButton.isVisible = text.trim().isEmpty() microphoneButton.isVisible = text.trim().isEmpty()
sendButton.isVisible = microphoneButton.isGone sendButton.isVisible = microphoneButton.isGone
@ -288,12 +277,9 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
fun setInputBarEditableFactory(factory: Editable.Factory) { fun setInputBarEditableFactory(factory: Editable.Factory) {
binding.inputBarEditText.setEditableFactory(factory) binding.inputBarEditText.setEditableFactory(factory)
} }
// endregion
} }
interface InputBarDelegate { interface InputBarDelegate {
fun inputBarEditTextContentChanged(newContent: CharSequence) fun inputBarEditTextContentChanged(newContent: CharSequence)
fun toggleAttachmentOptions() fun toggleAttachmentOptions()
fun showVoiceMessageUI() fun showVoiceMessageUI()
@ -303,4 +289,4 @@ interface InputBarDelegate {
fun onMicrophoneButtonUp(event: MotionEvent) fun onMicrophoneButtonUp(event: MotionEvent)
fun sendMessage() fun sendMessage()
fun commitInputContent(contentUri: Uri) fun commitInputContent(contentUri: Uri)
} }

View File

@ -57,7 +57,7 @@ object ConversationMenuHelper {
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) { if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
inflater.inflate(R.menu.menu_conversation_expiration, menu) 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) { if (thread.isContactRecipient) {
inflater.inflate(R.menu.menu_conversation_copy_account_id, menu) inflater.inflate(R.menu.menu_conversation_copy_account_id, menu)
} }
@ -325,7 +325,7 @@ object ConversationMenuHelper {
interface ConversationMenuListener { interface ConversationMenuListener {
fun block(deleteThread: Boolean = false) fun block(deleteThread: Boolean = false)
fun unblock() fun unblock()
fun copyAccountID(sessionId: String) fun copyAccountID(accountId: String)
fun copyOpenGroupUrl(thread: Recipient) fun copyOpenGroupUrl(thread: Recipient)
fun showDisappearingMessages(thread: Recipient) fun showDisappearingMessages(thread: Recipient)
} }

View File

@ -22,6 +22,7 @@ import androidx.core.view.isVisible
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
@ -289,7 +290,7 @@ class VisibleMessageContentView : ConstraintLayout {
// replace URLSpans with ModalURLSpans // replace URLSpans with ModalURLSpans
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan -> 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 replacementSpan = ModalURLSpan(updatedUrl) { url ->
val activity = context as AppCompatActivity val activity = context as AppCompatActivity
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog") ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")

View File

@ -1,29 +1,24 @@
package org.thoughtcrime.securesms.conversation.v2.utilities package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.util.Range import android.util.Range
import androidx.core.content.res.ResourcesCompat
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.combine.Tuple2 import nl.komponents.kovenant.combine.Tuple2
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.prefs import org.session.libsession.utilities.prefs
import org.session.libsession.utilities.truncateIdForDisplay
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.RoundedBackgroundSpan import org.thoughtcrime.securesms.util.RoundedBackgroundSpan
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
import org.thoughtcrime.securesms.util.toPx
import java.util.regex.Pattern import java.util.regex.Pattern
object MentionUtilities { object MentionUtilities {
@ -69,7 +64,7 @@ object MentionUtilities {
} else { } else {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey) val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR @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) { if (userDisplayName != null) {
val mention = "@$userDisplayName" val mention = "@$userDisplayName"
@ -162,7 +157,7 @@ object MentionUtilities {
} }
private fun isYou(mentionedPublicKey: String, userPublicKey: String, openGroup: OpenGroup?): Boolean { 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 return mentionedPublicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey
} }
} }

View File

@ -31,7 +31,7 @@ class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) :
private fun readBlindedIdMapping(cursor: Cursor): BlindedIdMapping { private fun readBlindedIdMapping(cursor: Cursor): BlindedIdMapping {
return BlindedIdMapping( return BlindedIdMapping(
blindedId = cursor.getString(cursor.getColumnIndexOrThrow(BLINDED_PK)), 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)), serverUrl = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_URL)),
serverId = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_PK)), serverId = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_PK)),
) )
@ -58,7 +58,7 @@ class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) :
try { try {
val values = ContentValues().apply { val values = ContentValues().apply {
put(BLINDED_PK, blindedIdMapping.blindedId) put(BLINDED_PK, blindedIdMapping.blindedId)
put(SERVER_PK, blindedIdMapping.sessionId) put(SERVER_PK, blindedIdMapping.accountId)
put(SERVER_URL, blindedIdMapping.serverUrl) put(SERVER_URL, blindedIdMapping.serverUrl)
put(SERVER_PK, blindedIdMapping.serverId) put(SERVER_PK, blindedIdMapping.serverId)
} }

View File

@ -1243,73 +1243,49 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
} }
private fun getNotificationMmsMessageRecord(cursor: Cursor): NotificationMmsMessageRecord { private fun getNotificationMmsMessageRecord(cursor: Cursor): NotificationMmsMessageRecord {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) // Note: Additional details such as ADDRESS_DEVICE_ID, CONTENT_LOCATION, and TRANSACTION_ID are available if required.
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)) val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
val dateReceived = cursor.getLong( val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
cursor.getColumnIndexOrThrow( val dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_RECEIVED))
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 threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)) val recipient = getRecipientFor(address)
val mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)) val messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_SIZE))
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)) val expiry = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRY))
val addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(ADDRESS_DEVICE_ID)) val status = cursor.getInt(cursor.getColumnIndexOrThrow(STATUS))
val recipient = getRecipientFor(address) val deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(DELIVERY_RECEIPT_COUNT))
val contentLocation = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)) val readReceiptCount = if (context.prefs.isReadReceiptsEnabled()) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0
val transactionId = cursor.getString(cursor.getColumnIndexOrThrow(TRANSACTION_ID)) val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
val messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_SIZE)) val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
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))
return NotificationMmsMessageRecord( return NotificationMmsMessageRecord(
id, recipient, recipient, id, recipient, recipient,
dateSent, dateReceived, deliveryReceiptCount, threadId, dateSent, dateReceived, deliveryReceiptCount, threadId,
contentLocationBytes, messageSize, expiry, status, messageSize, expiry, status, mailbox, slideDeck,
transactionIdBytes, mailbox, slideDeck,
readReceiptCount, hasMention readReceiptCount, hasMention
) )
} }
private fun getMediaMmsMessageRecord(cursor: Cursor, getQuote: Boolean): MediaMmsMessageRecord { private fun getMediaMmsMessageRecord(cursor: Cursor, getQuote: Boolean): MediaMmsMessageRecord {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)) val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
val dateReceived = cursor.getLong( val dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_RECEIVED))
cursor.getColumnIndexOrThrow( val box = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
NORMALIZED_DATE_RECEIVED 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 box = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)) val deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(DELIVERY_RECEIPT_COUNT))
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)) var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)) val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY))
val addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(ADDRESS_DEVICE_ID)) val partCount = cursor.getInt(cursor.getColumnIndexOrThrow(PART_COUNT))
val deliveryReceiptCount = cursor.getInt( val mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MISMATCHED_IDENTITIES))
cursor.getColumnIndexOrThrow( val networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(NETWORK_FAILURE))
DELIVERY_RECEIPT_COUNT val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
) val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN))
) val expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED))
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) val unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1
val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY)) val hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1
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()) { if (!context.prefs.isReadReceiptsEnabled()) {
readReceiptCount = 0 readReceiptCount = 0
} }

View File

@ -55,7 +55,7 @@ public class RecipientDatabase extends Database {
private static final String SYSTEM_PHONE_LABEL = "system_phone_label"; private static final String SYSTEM_PHONE_LABEL = "system_phone_label";
private static final String SYSTEM_CONTACT_URI = "system_contact_uri"; 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_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 PROFILE_SHARING = "profile_sharing_approval";
private static final String CALL_RINGTONE = "call_ringtone"; private static final String CALL_RINGTONE = "call_ringtone";
private static final String CALL_VIBRATE = "call_vibrate"; 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[] { 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, 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, 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, UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS 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, " + SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " + PROFILE_KEY + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_NAME + " 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, " + PROFILE_SHARING + " INTEGER DEFAULT 0, " +
CALL_RINGTONE + " TEXT DEFAULT NULL, " + CALL_RINGTONE + " TEXT DEFAULT NULL, " +
CALL_VIBRATE + " INTEGER DEFAULT " + Recipient.VibrateState.DEFAULT.getId() + ", " + 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 systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI)); String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME)); 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; boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL)); String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE)); 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) { public void setProfileAvatar(@NonNull Recipient recipient, @Nullable String profileAvatar) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar); contentValues.put(SESSION_PROFILE_AVATAR, profileAvatar);
updateOrInsert(recipient.getAddress(), contentValues); updateOrInsert(recipient.getAddress(), contentValues);
recipient.resolve().setProfileAvatar(profileAvatar); recipient.resolve().setProfileAvatar(profileAvatar);
notifyRecipientListeners(); notifyRecipientListeners();

View File

@ -15,7 +15,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
companion object { companion object {
private const val sessionContactTable = "session_contact_database" private const val sessionContactTable = "session_contact_database"
const val accountID = "account_id" const val accountID = "session_id"
const val name = "name" const val name = "name"
const val nickname = "nickname" const val nickname = "nickname"
const val profilePictureURL = "profile_picture_url" 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 val database = databaseHelper.readableDatabase
return database.getAll( return database.getAll(
sessionContactTable, sessionContactTable,
"$accountID IN (SELECT value FROM json_each(?))", "$accountID IN (SELECT value FROM json_each(?))",
arrayOf(JSONArray(sessionIDs).toString()) arrayOf(JSONArray(accountIDs).toString())
) { cursor -> contactFromCursor(cursor) } ) { cursor -> contactFromCursor(cursor) }
} }
@ -56,8 +56,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
return database.getAll(sessionContactTable, null, null) { cursor -> return database.getAll(sessionContactTable, null, null) { cursor ->
contactFromCursor(cursor) contactFromCursor(cursor)
}.filter { contact -> }.filter { contact ->
val sessionId = AccountId(contact.accountID) contact.accountID.let(::AccountId).prefix == IdPrefix.STANDARD
sessionId.prefix == IdPrefix.STANDARD
}.toSet() }.toSet()
} }

View File

@ -2,14 +2,17 @@ package org.thoughtcrime.securesms.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import java.security.MessageDigest
import network.loki.messenger.libsession_util.ConfigBase 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_HIDDEN
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED 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.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo 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.Conversation
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo 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.mms.PartAuthority
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.SessionMetaProtocol import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
private const val TAG = "Storage" private const val TAG = "Storage"
@ -113,12 +114,12 @@ open class Storage(
if (address.isGroup) { if (address.isGroup) {
val groups = configFactory.userGroups ?: return val groups = configFactory.userGroups ?: return
if (address.isClosedGroup) { if (address.isClosedGroup) {
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize()) val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
val closedGroup = getGroup(address.toGroupString()) val closedGroup = getGroup(address.toGroupString())
if (closedGroup != null && closedGroup.isActive) { if (closedGroup != null && closedGroup.isActive) {
val legacyGroup = groups.getOrConstructLegacyGroupInfo(sessionId) val legacyGroup = groups.getOrConstructLegacyGroupInfo(accountId)
groups.set(legacyGroup) groups.set(legacyGroup)
val newVolatileParams = volatile.getOrConstructLegacyGroup(sessionId).copy( val newVolatileParams = volatile.getOrConstructLegacyGroup(accountId).copy(
lastRead = SnodeAPI.nowWithOffset, lastRead = SnodeAPI.nowWithOffset,
) )
volatile.set(newVolatileParams) volatile.set(newVolatileParams)
@ -134,11 +135,11 @@ open class Storage(
if (getUserPublicKey() != address.serialize()) { if (getUserPublicKey() != address.serialize()) {
val contacts = configFactory.contacts ?: return val contacts = configFactory.contacts ?: return
contacts.upsertContact(address.serialize()) { contacts.upsertContact(address.serialize()) {
priority = ConfigBase.PRIORITY_VISIBLE priority = PRIORITY_VISIBLE
} }
} else { } else {
val userProfile = configFactory.user ?: return val userProfile = configFactory.user ?: return
userProfile.setNtsPriority(ConfigBase.PRIORITY_VISIBLE) userProfile.setNtsPriority(PRIORITY_VISIBLE)
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true) DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true)
} }
val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize()) val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize())
@ -151,9 +152,9 @@ open class Storage(
if (address.isGroup) { if (address.isGroup) {
val groups = configFactory.userGroups ?: return val groups = configFactory.userGroups ?: return
if (address.isClosedGroup) { if (address.isClosedGroup) {
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize()) val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
volatile.eraseLegacyClosedGroup(sessionId) volatile.eraseLegacyClosedGroup(accountId)
groups.eraseLegacyGroup(sessionId) groups.eraseLegacyGroup(accountId)
} else if (address.isCommunity) { } else if (address.isCommunity) {
// these should be removed in the group leave / handling new configs // these should be removed in the group leave / handling new configs
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere") Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
@ -267,10 +268,8 @@ open class Storage(
} }
// otherwise recipient is one to one // otherwise recipient is one to one
recipient.isContactRecipient -> { recipient.isContactRecipient -> {
// don't process non-standard session IDs though // don't process non-standard account IDs though
val sessionId = AccountId(recipient.address.serialize()) if (AccountId(recipient.address.serialize()).prefix != IdPrefix.STANDARD) return
if (sessionId.prefix != IdPrefix.STANDARD) return
config.getOrConstructOneToOne(recipient.address.serialize()) config.getOrConstructOneToOne(recipient.address.serialize())
} }
else -> throw NullPointerException("Weren't expecting to have a convo with address ${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 var messageID: Long? = null
val senderAddress = fromSerialized(message.sender!!) val senderAddress = fromSerialized(message.sender!!)
val isUserSender = (message.sender!! == getUserPublicKey()) val isUserSender = (message.sender!! == getUserPublicKey())
val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let { getOpenGroup(it)?.publicKey } val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let(::getOpenGroup)?.publicKey
?.let { SodiumUtilities.sessionId(getUserPublicKey()!!, message.sender!!, it) } ?: false ?.let { SodiumUtilities.accountId(getUserPublicKey()!!, message.sender!!, it) } ?: false
val group: Optional<SignalServiceGroup> = when { val group: Optional<SignalServiceGroup> = when {
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT)) openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
groupPublicKey != null -> { groupPublicKey != null -> {
@ -474,7 +473,8 @@ open class Storage(
val userPublicKey = getUserPublicKey() ?: return val userPublicKey = getUserPublicKey() ?: return
// would love to get rid of recipient and context from this // would love to get rid of recipient and context from this
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
// update name
// Update profile name
val name = userProfile.getName() ?: return val name = userProfile.getName() ?: return
val userPic = userProfile.getPic() val userPic = userProfile.getPic()
val profileManager = SSKEnvironment.shared.profileManager val profileManager = SSKEnvironment.shared.profileManager
@ -485,13 +485,14 @@ open class Storage(
if (it != name) userProfile.setName(it) if (it != name) userProfile.setName(it)
} }
// update pfp // Update profile picture
if (userPic == UserPic.DEFAULT) { if (userPic == UserPic.DEFAULT) {
clearUserPic() clearUserPic()
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty() } else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
&& context.prefs.getProfilePictureURL() != userPic.url) { && context.prefs.getProfilePictureURL() != userPic.url) {
setUserProfilePicture(userPic.url, userPic.key) setUserProfilePicture(userPic.url, userPic.key)
} }
if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) { if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) {
// delete nts thread if needed // delete nts thread if needed
val ourThread = getThreadId(recipient) ?: return val ourThread = getThreadId(recipient) ?: return
@ -519,12 +520,13 @@ open class Storage(
addLibSessionContacts(extracted, messageTimestamp) addLibSessionContacts(extracted, messageTimestamp)
} }
override fun clearUserPic() { override fun clearUserPic() {
val userPublicKey = getUserPublicKey() ?: return val userPublicKey = getUserPublicKey() ?: return Log.w(TAG, "No user public key when trying to clear user pic")
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase() 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) 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) context.prefs.setProfileKey(null)
ProfileKeyUtil.setEncodedProfileKey(context, null) ProfileKeyUtil.setEncodedProfileKey(context, null)
recipientDatabase.setProfileAvatar(recipient, null) recipientDatabase.setProfileAvatar(recipient, null)
@ -533,14 +535,13 @@ open class Storage(
Recipient.removeCached(fromSerialized(userPublicKey)) Recipient.removeCached(fromSerialized(userPublicKey))
configFactory.user?.setPic(UserPic.DEFAULT) configFactory.user?.setPic(UserPic.DEFAULT)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} }
private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) { private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) {
val extracted = convos.all() val extracted = convos.all()
for (conversation in extracted) { for (conversation in extracted) {
val threadId = when (conversation) { 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.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false)
is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", 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 existingJoinUrls = existingCommunities.values.map { it.joinURL }
val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup } 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 -> val toDeleteClosedGroups = existingClosedGroups.filter { group ->
GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
} }
@ -605,8 +606,8 @@ open class Storage(
} }
for (group in lgc) { for (group in lgc) {
val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId) val groupId = GroupUtil.doubleEncodeGroupID(group.accountId)
val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId } val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.accountId }
val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) } val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) }
if (existingGroup != null) { if (existingGroup != null) {
if (group.priority == PRIORITY_HIDDEN && existingThread != null) { if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
@ -625,19 +626,19 @@ open class Storage(
createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp) createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
setProfileSharing(Address.fromSerialized(groupId), true) setProfileSharing(Address.fromSerialized(groupId), true)
// Add the group to the user's set of public keys to poll for // 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 // Store the encryption key pair
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey)) 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 // Notify the PN server
PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey) PushRegistryV1.subscribeGroup(group.accountId, publicKey = localUserPublicKey)
// Notify the user // Notify the user
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId)) val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
threadDb.setDate(threadID, formationTimestamp) threadDb.setDate(threadID, formationTimestamp)
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, 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 // Don't create config group here, it's from a config update
// Start polling // Start polling
ClosedGroupPollerV2.shared.startPolling(group.sessionId) ClosedGroupPollerV2.shared.startPolling(group.accountId)
} }
getThreadId(Address.fromSerialized(groupId))?.let { getThreadId(Address.fromSerialized(groupId))?.let {
setExpirationConfiguration( setExpirationConfiguration(
@ -938,10 +939,10 @@ open class Storage(
groupVolatileConfig.lastRead = formationTimestamp groupVolatileConfig.lastRead = formationTimestamp
volatiles.set(groupVolatileConfig) volatiles.set(groupVolatileConfig)
val groupInfo = GroupInfo.LegacyGroupInfo( val groupInfo = GroupInfo.LegacyGroupInfo(
sessionId = groupPublicKey, accountId = groupPublicKey,
name = name, name = name,
members = members, members = members,
priority = ConfigBase.PRIORITY_VISIBLE, priority = PRIORITY_VISIBLE,
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = encryptionKeyPair.privateKey.serialize(), encSecKey = encryptionKeyPair.privateKey.serialize(),
disappearingTimer = expirationTimer.toLong(), disappearingTimer = expirationTimer.toLong(),
@ -975,7 +976,7 @@ open class Storage(
members = membersMap, members = membersMap,
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = latestKeyPair.privateKey.serialize(), 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, disappearingTimer = getExpirationConfiguration(threadID)?.expiryMode?.expirySeconds ?: 0L,
joinedAt = (existingGroup.formationTimestamp / 1000L) joinedAt = (existingGroup.formationTimestamp / 1000L)
) )
@ -1209,7 +1210,7 @@ open class Storage(
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
val moreContacts = contacts.filter { contact -> val moreContacts = contacts.filter { contact ->
val id = AccountId(contact.id) 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 val profileManager = SSKEnvironment.shared.profileManager
moreContacts.forEach { contact -> moreContacts.forEach { contact ->
@ -1262,7 +1263,7 @@ open class Storage(
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
val moreContacts = contacts.filter { contact -> val moreContacts = contacts.filter { contact ->
val id = AccountId(contact.publicKey) 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) { for (contact in moreContacts) {
val address = fromSerialized(contact.publicKey) val address = fromSerialized(contact.publicKey)
@ -1329,25 +1330,25 @@ open class Storage(
val threadRecipient = getRecipientForThread(threadID) ?: return val threadRecipient = getRecipientForThread(threadID) ?: return
if (threadRecipient.isLocalNumber) { if (threadRecipient.isLocalNumber) {
val user = configFactory.user ?: return 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) { } else if (threadRecipient.isContactRecipient) {
val contacts = configFactory.contacts ?: return val contacts = configFactory.contacts ?: return
contacts.upsertContact(threadRecipient.address.serialize()) { 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) { } else if (threadRecipient.isGroupRecipient) {
val groups = configFactory.userGroups ?: return val groups = configFactory.userGroups ?: return
if (threadRecipient.isClosedGroupRecipient) { if (threadRecipient.isClosedGroupRecipient) {
val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize()) threadRecipient.address.serialize()
val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy ( .let(GroupUtil::doubleDecodeGroupId)
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE .let(groups::getOrConstructLegacyGroupInfo)
) .copy (priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
groups.set(newGroupInfo) .let(groups::set)
} else if (threadRecipient.isCommunityRecipient) { } else if (threadRecipient.isCommunityRecipient) {
val openGroup = getOpenGroup(threadID) ?: return val openGroup = getOpenGroup(threadID) ?: return
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy ( val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE
) )
groups.set(newGroupInfo) groups.set(newGroupInfo)
} }
@ -1505,10 +1506,10 @@ open class Storage(
} }
} }
for (mapping in mappings) { 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 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)) val blindedThreadId = threadDB.getOrCreateThreadIdFor(Recipient.from(context, fromSerialized(mapping.key), false))
mmsDb.updateThreadId(blindedThreadId, threadId) mmsDb.updateThreadId(blindedThreadId, threadId)
@ -1614,20 +1615,20 @@ open class Storage(
): BlindedIdMapping { ): BlindedIdMapping {
val db = DatabaseComponent.get(context).blindedIdMappingDatabase() val db = DatabaseComponent.get(context).blindedIdMappingDatabase()
val mapping = db.getBlindedIdMapping(blindedId).firstOrNull() ?: BlindedIdMapping(blindedId, null, server, serverPublicKey) val mapping = db.getBlindedIdMapping(blindedId).firstOrNull() ?: BlindedIdMapping(blindedId, null, server, serverPublicKey)
if (mapping.sessionId != null) { if (mapping.accountId != null) {
return mapping return mapping
} }
getAllContacts().forEach { contact -> getAllContacts().forEach { contact ->
val sessionId = AccountId(contact.accountID) val accountId = AccountId(contact.accountID)
if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString, blindedId, serverPublicKey)) { if (accountId.prefix == IdPrefix.STANDARD && SodiumUtilities.accountId(accountId.hexString, blindedId, serverPublicKey)) {
val contactMapping = mapping.copy(sessionId = sessionId.hexString) val contactMapping = mapping.copy(accountId = accountId.hexString)
db.addBlindedIdMapping(contactMapping) db.addBlindedIdMapping(contactMapping)
return contactMapping return contactMapping
} }
} }
db.getBlindedIdMappingsExceptFor(server).forEach { db.getBlindedIdMappingsExceptFor(server).forEach {
if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) { if (SodiumUtilities.accountId(it.accountId!!, blindedId, serverPublicKey)) {
val otherMapping = mapping.copy(sessionId = it.sessionId) val otherMapping = mapping.copy(accountId = it.accountId)
db.addBlindedIdMapping(otherMapping) db.addBlindedIdMapping(otherMapping)
return otherMapping return otherMapping
} }

View File

@ -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));
}
}
}

View File

@ -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
}
}

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups
import android.content.Context import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.GroupMemberRole import org.session.libsession.messaging.open_groups.GroupMemberRole
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
@ -143,9 +144,9 @@ object OpenGroupManager {
@WorkerThread @WorkerThread
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? { 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 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 val publicKey = url.queryParameter("public_key") ?: return null
return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.text.SpannableString
import android.text.TextUtils import android.text.TextUtils
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue 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.RecipientDatabase.NOTIFY_TYPE_NONE
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
import org.thoughtcrime.securesms.util.getConversationUnread import org.thoughtcrime.securesms.util.getConversationUnread
@ -50,7 +48,7 @@ class ConversationView : LinearLayout {
// endregion // endregion
// region Updating // region Updating
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) { fun bind(thread: ThreadRecord, isTyping: Boolean) {
this.thread = thread this.thread = thread
if (thread.isPinned) { if (thread.isPinned) {
binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds( binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
@ -141,11 +139,10 @@ class ConversationView : LinearLayout {
else -> recipient.toShortString() // Internally uses the Contact API else -> recipient.toShortString() // Internally uses the Contact API
} }
private fun ThreadRecord.getSnippet(): CharSequence = private fun ThreadRecord.getSnippet(): CharSequence = listOfNotNull(
concatSnippet(getSnippetPrefix(), getDisplayBody(context)) getSnippetPrefix(),
getDisplayBody(context)
private fun concatSnippet(prefix: CharSequence?, body: CharSequence): CharSequence = ).joinToString(": ")
prefix?.let { TextUtils.concat(it, ": ", body) } ?: body
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when { private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null

View File

@ -21,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R 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.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter
import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout 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.home.search.GlobalSearchViewModel
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
@ -84,6 +86,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
GlobalSearchInputLayout.GlobalSearchInputLayoutListener { GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
companion object { companion object {
const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" 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 // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
if (!isTaskRoot) { finish(); return }
// Set content view // Set content view
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -171,7 +179,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up empty state view // Set up empty state view
binding.emptyStateContainer.setThemedContent { binding.emptyStateContainer.setThemedContent {
EmptyView(ApplicationContext.getInstance(this).newAccount) EmptyView(isNewAccount)
} }
IP2Country.configureIfNeeded(this@HomeActivity) IP2Country.configureIfNeeded(this@HomeActivity)
@ -231,67 +239,25 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
// Get group results and display them // Get group results and display them
launch { launch {
globalSearchViewModel.result.collect { result -> globalSearchViewModel.result.map { result ->
if (result.query.isEmpty()) { result.query to when {
class NamedValue<T>(val name: String?, val value: T) result.query.isEmpty() -> buildList {
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
// Unknown is temporarily to be grouped together with numbers title. add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
// https://optf.atlassian.net/browse/SES-2287 addAll(result.groupedContacts)
val numbersTitle = "#" }
val unknownTitle = numbersTitle else -> buildList {
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
listOf( add(GlobalSearchAdapter.Model.Header(R.string.contacts))
GlobalSearchAdapter.Model.Header(R.string.contacts), addAll(it)
GlobalSearchAdapter.Model.SavedMessages(publicKey) }
) + result.contacts result.messageResults.takeUnless { it.isEmpty() }?.let {
// Remove ourself, we're shown above. add(GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
.filter { it.accountID != publicKey } addAll(it)
// 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) }
} }
} 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 }.collectLatest(globalSearchAdapter::setNewData)
.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) }
}
} }
} }
EventBus.getDefault().register(this@HomeActivity) 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) { override fun onInputFocusChanged(hasFocus: Boolean) {
setSearchShown(hasFocus || binding.globalSearchInputLayout.query.value.isNotEmpty()) 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 { Intent(this, HomeActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(HomeActivity.NEW_ACCOUNT, true)
putExtra(HomeActivity.FROM_ONBOARDING, true) putExtra(HomeActivity.FROM_ONBOARDING, true)
}.also(::startActivity) }.also(::startActivity)
} }

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.home
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
@ -12,8 +11,6 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
class HomeAdapter( class HomeAdapter(
private val context: Context, private val context: Context,
@ -115,7 +112,7 @@ class HomeAdapter(
val offset = if (hasHeaderView()) position - 1 else position val offset = if (hasHeaderView()) position - 1 else position
val thread = data.threads[offset] val thread = data.threads[offset]
val isTyping = data.typingThreadIDs.contains(thread.threadId) val isTyping = data.typingThreadIDs.contains(thread.threadId)
holder.view.bind(thread, isTyping, glide) holder.view.bind(thread, isTyping)
} }
} }
} }

View File

@ -56,7 +56,6 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
with(binding) { with(binding) {
profilePictureView.publicKey = publicKey profilePictureView.publicKey = publicKey
profilePictureView.isLarge = true
profilePictureView.update(recipient) profilePictureView.update(recipient)
nameTextViewContainer.visibility = View.VISIBLE nameTextViewContainer.visibility = View.VISIBLE
nameTextViewContainer.setOnClickListener { nameTextViewContainer.setOnClickListener {

View File

@ -28,6 +28,8 @@ class GlobalSearchAdapter(private val modelCallback: (Model)->Unit): RecyclerVie
private var data: List<Model> = listOf() private var data: List<Model> = listOf()
private var query: String? = null private var query: String? = null
fun setNewData(data: Pair<String, List<Model>>) = setNewData(data.first, data.second)
fun setNewData(query: String, newData: List<Model>) { fun setNewData(query: String, newData: List<Model>) {
val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData)) val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData))
this.query = query this.query = query
@ -134,7 +136,7 @@ class GlobalSearchAdapter(private val modelCallback: (Model)->Unit): RecyclerVie
constructor(title: String): this(GetString(title)) constructor(title: String): this(GetString(title))
} }
data class SavedMessages(val currentUserPublicKey: String): Model() 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 GroupConversation(val groupRecord: GroupRecord): Model()
data class Message(val messageResult: MessageResult, val unread: Int, val isSelf: Boolean): Model() data class Message(val messageResult: MessageResult, val unread: Int, val isSelf: Boolean): Model()
} }

View File

@ -6,22 +6,18 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.session.libsignal.utilities.SettableFuture import org.session.libsignal.utilities.SettableFuture
@ -35,16 +31,32 @@ import javax.inject.Inject
class GlobalSearchViewModel @Inject constructor( class GlobalSearchViewModel @Inject constructor(
private val searchRepository: SearchRepository, private val searchRepository: SearchRepository,
) : ViewModel() { ) : ViewModel() {
private val scope = viewModelScope + SupervisorJob()
private val executor = viewModelScope + SupervisorJob()
private val _result: MutableStateFlow<GlobalSearchResult> = MutableStateFlow(GlobalSearchResult.EMPTY)
val result: StateFlow<GlobalSearchResult> = _result
private val refreshes = MutableSharedFlow<Unit>() 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) { fun setQuery(charSequence: CharSequence) {
_queryText.value = charSequence _queryText.value = charSequence
@ -55,34 +67,6 @@ class GlobalSearchViewModel @Inject constructor(
refreshes.emit(Unit) 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)
}
} }
/** /**

View File

@ -35,6 +35,5 @@ public class AndroidLogger extends Log.Logger {
} }
@Override @Override
public void blockUntilAllWritesFinished() { public void blockUntilAllWritesFinished() { }
}
} }

View File

@ -34,13 +34,13 @@ class MessageRequestView : LinearLayout {
// region Updating // region Updating
fun bind(thread: ThreadRecord, glide: GlideRequests) { fun bind(thread: ThreadRecord, glide: GlideRequests) {
this.thread = thread 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.displayNameTextView.text = senderDisplayName
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
val rawSnippet = thread.getDisplayBody(context)
val snippet = highlightMentions( val snippet = highlightMentions(
text = rawSnippet, text = thread.getDisplayBody(context),
formatOnly = true, // no styling here, only text formatting formatOnly = true, // no styling here, only text formatting
threadID = thread.threadId, threadID = thread.threadId,
context = context context = context

View File

@ -16,24 +16,23 @@
*/ */
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import static org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY;
import android.content.Context; import android.content.Context;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.squareup.phrase.Phrase;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import java.security.SecureRandom;
import org.thoughtcrime.securesms.util.MediaUtil; import network.loki.messenger.R;
import org.session.libsignal.utilities.guava.Optional;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; 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.messaging.sending_receiving.attachments.UriAttachment;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsignal.utilities.guava.Optional;
import java.security.SecureRandom; import org.thoughtcrime.securesms.util.MediaUtil;
import network.loki.messenger.R;
public abstract class Slide { public abstract class Slide {
@ -72,20 +71,23 @@ public abstract class Slide {
return Optional.fromNullable("🎤 " + attachmentString); 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() { private String emojiForMimeType() {
if (MediaUtil.isImage(attachment)) { if (MediaUtil.isImage(attachment)) {
return "📷 "; return "📷";
} else if (MediaUtil.isVideo(attachment)) { } else if (MediaUtil.isVideo(attachment)) {
return "🎥 "; return "🎥";
} else if (MediaUtil.isAudio(attachment)) { } else if (MediaUtil.isAudio(attachment)) {
return "🎧 "; return "🎧";
} else if (MediaUtil.isFile(attachment)) { } else if (MediaUtil.isFile(attachment)) {
return "📎 "; return "📎";
} else { } else {
return "🎡 "; return "🎡"; // `isGif`
} }
} }
@ -155,20 +157,20 @@ public abstract class Slide {
return false; return false;
} }
protected static Attachment constructAttachmentFromUri(@NonNull Context context, protected static Attachment constructAttachmentFromUri(@NonNull Context context,
@NonNull Uri uri, @NonNull Uri uri,
@NonNull String defaultMime, @NonNull String defaultMime,
long size, long size,
int width, int width,
int height, int height,
boolean hasThumbnail, boolean hasThumbnail,
@Nullable String fileName, @Nullable String fileName,
@Nullable String caption, @Nullable String caption,
boolean voiceNote, boolean voiceNote,
boolean quote) boolean quote)
{ {
String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime); String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime);
String fastPreflightId = String.valueOf(new SecureRandom().nextLong()); String fastPreflightId = String.valueOf(new SecureRandom().nextLong());
return new UriAttachment(uri, return new UriAttachment(uri,
hasThumbnail ? uri : null, hasThumbnail ? uri : null,
resolvedType, resolvedType,

View File

@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
@ -47,8 +48,7 @@ public class SlideDeck {
if (slide != null) slides.add(slide); if (slide != null) slides.add(slide);
} }
public SlideDeck() { public SlideDeck() { }
}
public void clear() { public void clear() {
slides.clear(); slides.clear();
@ -65,7 +65,6 @@ public class SlideDeck {
body = slideBody.get(); body = slideBody.get();
} }
} }
return body; return body;
} }

View File

@ -29,6 +29,7 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.service.notification.StatusBarNotification; import android.service.notification.StatusBarNotification;
import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -146,9 +147,8 @@ public class DefaultMessageNotifier implements MessageNotifier {
} }
public void notifyMessagesPending(Context context) { 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()); PendingMessageNotificationBuilder builder = new PendingMessageNotificationBuilder(context, MessagingModuleConfiguration.getShared().getPrefs().getNotificationPrivacy());
ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build()); ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build());
@ -186,9 +186,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
for (StatusBarNotification notification : activeNotifications) { for (StatusBarNotification notification : activeNotifications) {
boolean validNotification = false; boolean validNotification = false;
if (notification.getId() != SUMMARY_NOTIFICATION_ID && if (notification.getId() != SUMMARY_NOTIFICATION_ID &&
notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && notification.getId() != KeyCachingService.SERVICE_RUNNING_ID &&
notification.getId() != FOREGROUND_ID && notification.getId() != FOREGROUND_ID &&
notification.getId() != PENDING_MESSAGES_ID) notification.getId() != PENDING_MESSAGES_ID)
{ {
for (NotificationItem item : notificationState.getNotifications()) { for (NotificationItem item : notificationState.getNotifications()) {
@ -198,9 +198,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
} }
} }
if (!validNotification) { if (!validNotification) { notifications.cancel(notification.getId()); }
notifications.cancel(notification.getId());
}
} }
} }
} catch (Throwable e) { } catch (Throwable e) {
@ -232,7 +230,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
@Override @Override
public void updateNotification(@NonNull Context context, long threadId, boolean signal) public void updateNotification(@NonNull Context context, long threadId, boolean signal)
{ {
boolean isVisible = visibleThread == threadId; boolean isVisible = visibleThread == threadId;
ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase(); ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase();
Recipient recipient = threads.getRecipientForThreadId(threadId); Recipient recipient = threads.getRecipientForThreadId(threadId);
@ -349,14 +347,19 @@ public class DefaultMessageNotifier implements MessageNotifier {
builder.setThread(notifications.get(0).getRecipient()); builder.setThread(notifications.get(0).getRecipient());
builder.setMessageCount(notificationState.getMessageCount()); builder.setMessageCount(notificationState.getMessageCount());
// TODO: Removing highlighting mentions in the notification because this context is the libsession one which CharSequence builderCS = text == null ? "" : text;
// TODO: doesn't have access to the `R.attr.message_sent_text_color` and `R.attr.message_received_text_color` SpannableString ss = MentionUtilities.highlightMentions(
// TODO: attributes to perform the colour lookup. Also, it makes little sense to highlight the mentions using builderCS,
// TODO: the app theme as it may result in insufficient contrast with the notification background which will false,
// TODO: be using the SYSTEM theme. false,
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(), true,
//MentionUtilities.highlightMentions(text == null ? "" : text, notifications.get(0).getThreadId(), context), // Removing hightlighting mentions -ACL bundled ? notifications.get(0).getThreadId() : 0,
text == null ? "" : text, context
);
builder.setPrimaryMessageBody(recipient,
notifications.get(0).getIndividualRecipient(),
ss,
notifications.get(0).getSlideDeck()); notifications.get(0).getSlideDeck());
builder.setContentIntent(notifications.get(0).getPendingIntent(context)); builder.setContentIntent(notifications.get(0).getPendingIntent(context));
@ -506,24 +509,39 @@ public class DefaultMessageNotifier implements MessageNotifier {
continue; continue;
} }
} }
// If this is a message request from an unknown user..
if (messageRequest) { if (messageRequest) {
body = SpanUtil.italic(context.getString(R.string.message_requests_notification)); 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)) { } else if (KeyCachingService.isLocked(context)) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); 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()) { } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
body = ContactUtil.getStringSummary(context, contact); 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()) { } else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
body = SpanUtil.italic(slideDeck.getBody()); 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()) { } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
String message = slideDeck.getBody() + ": " + record.getBody(); String message = slideDeck.getBody() + ": " + record.getBody();
int italicLength = message.length() - body.length(); int italicLength = message.length() - body.length();
body = SpanUtil.italic(message, italicLength); body = SpanUtil.italic(message, italicLength);
// If this is a notification about an invitation to a community..
} else if (record.isOpenGroupInvitation()) { } else if (record.isOpenGroupInvitation()) {
body = SpanUtil.italic(context.getString(R.string.ThreadRecord_open_group_invitation)); body = SpanUtil.italic(context.getString(R.string.ThreadRecord_open_group_invitation));
} }
String userPublicKey = MessagingModuleConfiguration.getShared().getPrefs().getLocalNumber(); String userPublicKey = MessagingModuleConfiguration.getShared().getPrefs().getLocalNumber();
String blindedPublicKey = cache.get(threadId); String blindedPublicKey = cache.get(threadId);
if (blindedPublicKey == null) { if (blindedPublicKey == null) {

View File

@ -1,11 +1,9 @@
package org.thoughtcrime.securesms.notifications; package org.thoughtcrime.securesms.notifications;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;

View File

@ -10,6 +10,7 @@ import kotlinx.serialization.json.decodeFromStream
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.Response 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> { private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
val server = Server.LATEST val server = Server.LATEST
val url = "${server.url}/$path" 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() val request = Request.Builder().url(url).post(body).build()
return OnionRequestAPI.sendOnionRequest( return OnionRequestAPI.sendOnionRequest(

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.onboarding package org.thoughtcrime.securesms.onboarding
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import network.loki.messenger.R import network.loki.messenger.R
@ -11,12 +12,13 @@ import org.thoughtcrime.securesms.ui.color.LocalColors
@Composable @Composable
fun OnboardingBackPressAlertDialog( fun OnboardingBackPressAlertDialog(
dismissDialog: () -> Unit, 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 quit: () -> Unit
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = dismissDialog, onDismissRequest = dismissDialog,
title = stringResource(R.string.warning), 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( buttons = listOf(
DialogButtonModel( DialogButtonModel(
GetString(stringResource(R.string.quit)), GetString(stringResource(R.string.quit)),

View File

@ -1,14 +1,10 @@
package org.thoughtcrime.securesms.onboarding.loading 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.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -20,21 +16,11 @@ import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.h7 import org.thoughtcrime.securesms.ui.h7
@Composable @Composable
internal fun LoadingScreen(state: State) { internal fun LoadingScreen(progress: Float) {
val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) }
LaunchedEffect(state) {
animatable.stop()
animatable.animateTo(
targetValue = 1f,
animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt())
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
ProgressArc( ProgressArc(
animatable.value, progress,
modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation) modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation)
) )
Text( Text(

View File

@ -8,7 +8,6 @@ import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.home.startHomeActivity import org.thoughtcrime.securesms.home.startHomeActivity
@ -31,7 +30,7 @@ class LoadingActivity: BaseActionBarActivity() {
private fun register(loadFailed: Boolean) { private fun register(loadFailed: Boolean) {
when { when {
loadFailed -> startPickDisplayNameActivity(loadFailed = true) loadFailed -> startPickDisplayNameActivity(loadFailed = true)
else -> startHomeActivity() else -> startHomeActivity(isNewAccount = false)
} }
finish() finish()
@ -42,11 +41,9 @@ class LoadingActivity: BaseActionBarActivity() {
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
ApplicationContext.getInstance(this).newAccount = false
setComposeContent { setComposeContent {
val state by viewModel.states.collectAsState() val progress by viewModel.progress.collectAsState()
LoadingScreen(state) LoadingScreen(progress)
} }
lifecycleScope.launch { lifecycleScope.launch {

View File

@ -4,14 +4,21 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter 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.flow.timeout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -21,25 +28,43 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds 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 ANIMATE_TO_DONE_TIME = 500.milliseconds
private val IDLE_DONE_TIME = 1.seconds private val IDLE_DONE_TIME = 1.seconds
private val TIMEOUT_TIME = 15.seconds private val TIMEOUT_TIME = 15.seconds
@OptIn(FlowPreview::class) private val REFRESH_TIME = 50.milliseconds
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
internal class LoadingViewModel @Inject constructor( internal class LoadingViewModel @Inject constructor(
val prefs: TextSecurePreferences val prefs: TextSecurePreferences
): ViewModel() { ): ViewModel() {
private val _states = MutableStateFlow(State(TIMEOUT_TIME)) private val state = MutableStateFlow(State.LOADING)
val states = _states.asStateFlow()
private val _progress = MutableStateFlow(0f)
val progress = _progress.asStateFlow()
private val _events = MutableSharedFlow<Event>() private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow() val events = _events.asSharedFlow()
init { 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
prefs.configurationMessageSyncedFlow() prefs.configurationMessageSyncedFlow()
@ -54,7 +79,7 @@ internal class LoadingViewModel @Inject constructor(
private suspend fun onSuccess() { private suspend fun onSuccess() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_states.value = State(ANIMATE_TO_DONE_TIME) state.value = State.SUCCESS
delay(IDLE_DONE_TIME) delay(IDLE_DONE_TIME)
_events.emit(Event.SUCCESS) _events.emit(Event.SUCCESS)
} }
@ -62,6 +87,8 @@ internal class LoadingViewModel @Inject constructor(
private suspend fun onFail() { private suspend fun onFail() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
state.value = State.FAIL
delay(IDLE_DONE_TIME)
_events.emit(Event.TIMEOUT) _events.emit(Event.TIMEOUT)
} }
} }
@ -71,3 +98,22 @@ sealed interface Event {
object SUCCESS: Event object SUCCESS: Event
object TIMEOUT: 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)
}

View File

@ -58,7 +58,7 @@ internal fun MessageNotificationsScreen(
return return
} }
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit) if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit = quit)
Column { Column {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))

View File

@ -49,7 +49,7 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
viewModel.events.collect { viewModel.events.collect {
when (it) { when (it) {
Event.Loading -> start<LoadingActivity>() Event.Loading -> start<LoadingActivity>()
Event.OnboardingComplete -> startHomeActivity() Event.OnboardingComplete -> startHomeActivity(isNewAccount = true)
} }
} }
} }

View File

@ -40,7 +40,11 @@ internal fun PickDisplayName(
quit: () -> Unit = {} 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( Column(
modifier = Modifier modifier = Modifier

View File

@ -45,7 +45,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
viewModel.events.collect { viewModel.events.collect {
when (it) { when (it) {
is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName) 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) { fun Context.startPickDisplayNameActivity(loadFailed: Boolean = false, flags: Int = 0) {
ApplicationContext.getInstance(this).newAccount = !loadFailed
Intent(this, PickDisplayNameActivity::class.java) Intent(this, PickDisplayNameActivity::class.java)
.apply { putExtra(EXTRA_LOAD_FAILED, loadFailed) } .apply { putExtra(EXTRA_LOAD_FAILED, loadFailed) }
.also { it.flags = flags } .also { it.flags = flags }

View File

@ -74,7 +74,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity() {
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @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 } val pagerState = rememberPagerState { TITLES.size }
Column { Column {
@ -84,7 +84,7 @@ private fun Tabs(sessionId: String, errors: Flow<String>, onScan: (String) -> Un
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { page -> ) { page ->
when (TITLES[page]) { when (TITLES[page]) {
R.string.view -> QrPage(sessionId) R.string.view -> QrPage(accountId)
R.string.scan -> MaybeScanQrCode(errors, onScan = onScan) R.string.scan -> MaybeScanQrCode(errors, onScan = onScan)
} }
} }

View File

@ -46,9 +46,8 @@ import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding import network.loki.messenger.databinding.ActivitySettingsBinding
import network.loki.messenger.libsession_util.util.UserPic 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.alwaysUi
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.avatars.ProfileContactPhoto 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.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.NetworkUtils
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import java.io.File import java.io.File
import java.security.SecureRandom import java.security.SecureRandom
import javax.inject.Inject import javax.inject.Inject
private const val TAG = "SettingsActivity"
@AndroidEntryPoint @AndroidEntryPoint
class SettingsActivity : PassphraseRequiredActionBarActivity() { class SettingsActivity : PassphraseRequiredActionBarActivity() {
private val TAG = "SettingsActivity"
@Inject @Inject
lateinit var configFactory: ConfigFactory lateinit var configFactory: ConfigFactory
@ -146,7 +145,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
view.apply { view.apply {
publicKey = hexEncodedPublicKey publicKey = hexEncodedPublicKey
displayName = getDisplayName() displayName = getDisplayName()
isLarge = true
update() update()
} }
} }
@ -198,7 +196,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
try { try {
val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
updateProfile(true, profilePictureToBeUploaded) updateProfilePicture(profilePictureToBeUploaded)
} }
} catch (e: BitmapDecodingException) { } catch (e: BitmapDecodingException) {
Log.e(TAG, e) Log.e(TAG, e)
@ -247,54 +245,118 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
} }
private fun updateProfile( private fun updateDisplayName(displayName: String): Boolean {
isUpdatingProfilePicture: Boolean,
profilePicture: ByteArray? = null,
displayName: String? = null
) {
binding.loader.isVisible = true 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) prefs.setProfileName(displayName)
configFactory.user?.setName(displayName) val user = configFactory.user
} if (user == null) {
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) Log.w(TAG, "Cannot update display name - missing user details from configFactory.")
if (isUpdatingProfilePicture) {
if (profilePicture != null) {
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
} else { } 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() 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 val userConfig = configFactory.user
if (isUpdatingProfilePicture) { AvatarHelper.setAvatar(this, Address.fromSerialized(prefs.getLocalNumber()!!), profilePicture)
AvatarHelper.setAvatar(this, Address.fromSerialized(prefs.getLocalNumber()!!), profilePicture) prefs.setProfileAvatarId(SecureRandom().nextInt() )
prefs.setProfileAvatarId(profilePicture?.let { SecureRandom().nextInt() } ?: 0 ) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// new config // Attempt to grab the details we require to update the profile picture
val url = prefs.getProfilePictureURL() val url = prefs.getProfilePictureURL()
val profileKey = ProfileKeyUtil.getProfileKey(this) val profileKey = ProfileKeyUtil.getProfileKey(this)
if (profilePicture == null) {
userConfig?.setPic(UserPic.DEFAULT) // If we have a URL and a profile key then set the user's profile picture
} else if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) { if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
userConfig?.setPic(UserPic(url, profileKey)) userConfig?.setPic(UserPic(url, profileKey))
}
} }
if (userConfig != null && userConfig.needsDump()) { if (userConfig != null && userConfig.needsDump()) {
configFactory.persist(userConfig, SnodeAPI.nowWithOffset) configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
} }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
} alwaysUi {
if (displayName != null) { // Update our visuals
binding.btnGroupNameDisplay.text = displayName binding.profilePictureView.recycle()
} binding.profilePictureView.update()
if (isUpdatingProfilePicture) {
binding.profilePictureView.recycle() // Clear the cached image before updating
binding.profilePictureView.update()
}
binding.loader.isVisible = false
} }
// 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 // endregion
@ -313,8 +375,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show()
return false return false
} }
updateProfile(false, displayName = displayName) return updateDisplayName(displayName)
return true
} }
private fun showEditProfilePictureUI() { private fun showEditProfilePictureUI() {
@ -323,7 +384,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
view(R.layout.dialog_change_avatar) view(R.layout.dialog_change_avatar)
button(R.string.activity_settings_upload) { startAvatarSelection() } button(R.string.activity_settings_upload) { startAvatarSelection() }
if (prefs.getProfileAvatarId() != 0) { if (prefs.getProfileAvatarId() != 0) {
button(R.string.activity_settings_remove) { removeAvatar() } button(R.string.activity_settings_remove) { removeProfilePicture() }
} }
cancelButton() cancelButton()
}.apply { }.apply {
@ -341,10 +402,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
} }
private fun removeAvatar() {
updateProfile(true)
}
private fun startAvatarSelection() { private fun startAvatarSelection() {
// Ask for an optional camera permission. // Ask for an optional camera permission.
Permissions.with(this) Permissions.with(this)

View File

@ -293,7 +293,7 @@ class DefaultConversationRepository @Inject constructor(
override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> = override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf<Unit> =
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
// Note: This sessionId could be the blinded Id // Note: This accountId could be the blinded Id
val accountID = recipient.address.toString() val accountID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!

View File

@ -81,6 +81,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID" const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID"
const val EXTRA_ENABLED = "ENABLED" const val EXTRA_ENABLED = "ENABLED"
const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND" const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND"
const val EXTRA_SWAPPED = "is_video_swapped"
const val EXTRA_MUTE = "mute_value" const val EXTRA_MUTE = "mute_value"
const val EXTRA_AVAILABLE = "enabled_value" const val EXTRA_AVAILABLE = "enabled_value"
const val EXTRA_REMOTE_DESCRIPTION = "remote_description" const val EXTRA_REMOTE_DESCRIPTION = "remote_description"

View File

@ -94,8 +94,8 @@ class ProfileManager(private val context: Context, private val configFactory: Co
override fun contactUpdatedInternal(contact: Contact): String? { override fun contactUpdatedInternal(contact: Contact): String? {
val contactConfig = configFactory.contacts ?: return null val contactConfig = configFactory.contacts ?: return null
if (contact.accountID == context.prefs.getLocalNumber()) return null if (contact.accountID == context.prefs.getLocalNumber()) return null
val sessionId = AccountId(contact.accountID) val accountId = AccountId(contact.accountID)
if (sessionId.prefix != IdPrefix.STANDARD) return null // only internally store standard session IDs if (accountId.prefix != IdPrefix.STANDARD) return null // only internally store standard account IDs
contactConfig.upsertContact(contact.accountID) { contactConfig.upsertContact(contact.accountID) {
this.name = contact.name.orEmpty() this.name = contact.name.orEmpty()
this.nickname = contact.nickname.orEmpty() this.nickname = contact.nickname.orEmpty()

View File

@ -24,6 +24,7 @@ import org.session.libsession.utilities.prefs
import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
@ -31,10 +32,12 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.util.Timer import java.util.Timer
object ConfigurationMessageUtilities { object ConfigurationMessageUtilities {
private const val TAG = "ConfigMessageUtils"
private val debouncer = WindowDebouncer(3000, Timer()) private val debouncer = WindowDebouncer(3000, Timer())
private fun scheduleConfigSync(userPublicKey: String) { private fun scheduleConfigSync(userPublicKey: String) {
debouncer.publish { debouncer.publish {
// don't schedule job if we already have one // don't schedule job if we already have one
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
@ -44,23 +47,20 @@ object ConfigurationMessageUtilities {
(currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true) (currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true)
return@publish return@publish
} }
val newConfigSync = ConfigurationSyncJob(ourDestination) val newConfigSyncJob = ConfigurationSyncJob(ourDestination)
JobQueue.shared.add(newConfigSync) JobQueue.shared.add(newConfigSyncJob)
} }
} }
@JvmStatic @JvmStatic
fun syncConfigurationIfNeeded(context: Context) { fun syncConfigurationIfNeeded(context: Context) {
// add if check here to schedule new config job process and return early val userPublicKey = context.prefs.getLocalNumber() ?: return Log.w(TAG, "User Public Key is null")
val userPublicKey = context.prefs.getLocalNumber() ?: return
scheduleConfigSync(userPublicKey) scheduleConfigSync(userPublicKey)
} }
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> { 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")) val userPublicKey = context.prefs.getLocalNumber() ?: return Promise.ofFail(NullPointerException("User Public Key is null"))
// schedule job if none exist // Schedule a new job if one doesn't already exist (only)
// don't schedule job if we already have one
scheduleConfigSync(userPublicKey) scheduleConfigSync(userPublicKey)
return Promise.ofSuccess(Unit) return Promise.ofSuccess(Unit)
} }
@ -205,7 +205,7 @@ object ConfigurationMessageUtilities {
val admins = group.admins.map { it.serialize() to true }.toMap() 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() val members = group.members.filterNot { it.serialize() !in admins.keys }.map { it.serialize() to false }.toMap()
GroupInfo.LegacyGroupInfo( GroupInfo.LegacyGroupInfo(
sessionId = groupPublicKey, accountId = groupPublicKey,
name = group.title, name = group.title,
members = admins + members, members = admins + members,
priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,

View File

@ -99,7 +99,7 @@ class IP2Country private constructor(private val context: Context) {
val bestMatchCountry = comps.lastOrNull { it.key <= Ipv4Int(ip) }?.let { (_, code) -> val bestMatchCountry = comps.lastOrNull { it.key <= Ipv4Int(ip) }?.let { (_, code) ->
if (code != null) { if (code != null) {
countryToNames[code] + " [" + ip + "]" countryToNames[code]
} else { } else {
null null
} }

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.util package org.thoughtcrime.securesms.util
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
@ -50,6 +49,17 @@ class RoundedBackgroundSpan(
override fun getSize( override fun getSize(
paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt? paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?
): Int { ): 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() return (paint.measureText(text, start, end) + 2 * paddingHorizontal).toInt()
} }

View File

@ -6,11 +6,14 @@ import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
@ -51,12 +54,14 @@ import org.webrtc.MediaStream
import org.webrtc.PeerConnection import org.webrtc.PeerConnection
import org.webrtc.PeerConnection.IceConnectionState import org.webrtc.PeerConnection.IceConnectionState
import org.webrtc.PeerConnectionFactory import org.webrtc.PeerConnectionFactory
import org.webrtc.RendererCommon
import org.webrtc.RtpReceiver import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription import org.webrtc.SessionDescription
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.UUID import java.util.UUID
import kotlin.math.abs
import org.thoughtcrime.securesms.webrtc.data.State as CallState import org.thoughtcrime.securesms.webrtc.data.State as CallState
class CallManager( class CallManager(
@ -105,10 +110,15 @@ class CallManager(
private val _audioEvents = MutableStateFlow(AudioEnabled(false)) private val _audioEvents = MutableStateFlow(AudioEnabled(false))
val audioEvents = _audioEvents.asSharedFlow() val audioEvents = _audioEvents.asSharedFlow()
private val _videoEvents = MutableStateFlow(VideoEnabled(false))
val videoEvents = _videoEvents.asSharedFlow() private val _videoState: MutableStateFlow<VideoState> = MutableStateFlow(
private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false)) VideoState(
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow() swapped = false,
userVideoEnabled = false,
remoteVideoEnabled = false
)
)
val videoState = _videoState
private val stateProcessor = StateProcessor(CallState.Idle) private val stateProcessor = StateProcessor(CallState.Idle)
@ -151,9 +161,9 @@ class CallManager(
private val outgoingIceDebouncer = Debouncer(200L) private val outgoingIceDebouncer = Debouncer(200L)
var localRenderer: SurfaceViewRenderer? = null var floatingRenderer: SurfaceViewRenderer? = null
var remoteRotationSink: RemoteRotationVideoProxySink? = null var remoteRotationSink: RemoteRotationVideoProxySink? = null
var remoteRenderer: SurfaceViewRenderer? = null var fullscreenRenderer: SurfaceViewRenderer? = null
private var peerConnectionFactory: PeerConnectionFactory? = null private var peerConnectionFactory: PeerConnectionFactory? = null
fun clearPendingIceUpdates() { fun clearPendingIceUpdates() {
@ -216,20 +226,18 @@ class CallManager(
Util.runOnMainSync { Util.runOnMainSync {
val base = EglBase.create() val base = EglBase.create()
eglBase = base eglBase = base
localRenderer = SurfaceViewRenderer(context).apply { floatingRenderer = SurfaceViewRenderer(context)
// setScalingType(SCALE_ASPECT_FIT) 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() remoteRotationSink = RemoteRotationVideoProxySink()
localRenderer?.init(base.eglBaseContext, null) floatingRenderer?.init(base.eglBaseContext, null)
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT) fullscreenRenderer?.init(base.eglBaseContext, null)
remoteRenderer?.init(base.eglBaseContext, null) remoteRotationSink!!.setSink(fullscreenRenderer!!)
remoteRotationSink!!.setSink(remoteRenderer!!)
val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true) val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true)
val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext) val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext)
@ -363,7 +371,8 @@ class CallManager(
val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] } val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] }
val json = Json.parseToJsonElement(byteArray.decodeToString()) as JsonObject val json = Json.parseToJsonElement(byteArray.decodeToString()) as JsonObject
if (json.containsKey("video")) { 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")) { } else if (json.containsKey("hangup")) {
peerConnectionObservers.forEach(WebRtcListener::onHangup) peerConnectionObservers.forEach(WebRtcListener::onHangup)
} }
@ -383,13 +392,13 @@ class CallManager(
peerConnection?.dispose() peerConnection?.dispose()
peerConnection = null peerConnection = null
localRenderer?.release() floatingRenderer?.release()
remoteRotationSink?.release() remoteRotationSink?.release()
remoteRenderer?.release() fullscreenRenderer?.release()
eglBase?.release() eglBase?.release()
localRenderer = null floatingRenderer = null
remoteRenderer = null fullscreenRenderer = null
eglBase = null eglBase = null
localCameraState = CameraState.UNKNOWN localCameraState = CameraState.UNKNOWN
@ -399,8 +408,11 @@ class CallManager(
pendingOffer = null pendingOffer = null
callStartTime = -1 callStartTime = -1
_audioEvents.value = AudioEnabled(false) _audioEvents.value = AudioEnabled(false)
_videoEvents.value = VideoEnabled(false) _videoState.value = VideoState(
_remoteVideoEvents.value = VideoEnabled(false) swapped = false,
userVideoEnabled = false,
remoteVideoEnabled = false
)
pendingOutgoingIceUpdates.clear() pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.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 // 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. // 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) { 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 recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null"))
val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null")) val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null"))
val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory 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 base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
val connection = PeerConnectionWrapper( val connection = PeerConnectionWrapper(
context, context,
@ -515,7 +527,7 @@ class CallManager(
?: return Promise.ofFail(NullPointerException("recipient is null")) ?: return Promise.ofFail(NullPointerException("recipient is null"))
val factory = peerConnectionFactory val factory = peerConnectionFactory
?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
val local = localRenderer val local = floatingRenderer
?: return Promise.ofFail(NullPointerException("localRenderer is null")) ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase 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) { fun handleSetMuteAudio(muted: Boolean) {
_audioEvents.value = AudioEnabled(!muted) _audioEvents.value = AudioEnabled(!muted)
peerConnection?.setAudioEnabled(!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) { fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) {
_videoEvents.value = VideoEnabled(!muted) _videoState.update { it.copy(userVideoEnabled = !muted) }
handleMirroring()
val connection = peerConnection ?: return val connection = peerConnection ?: return
connection.setVideoEnabled(!muted) connection.setVideoEnabled(!muted)
dataChannel?.let { channel -> dataChannel?.let { channel ->
@ -651,9 +708,18 @@ class CallManager(
} }
} }
fun setDeviceRotation(newRotation: Int) { fun setDeviceOrientation(orientation: Orientation) {
peerConnection?.setDeviceRotation(newRotation) // set rotation to the video based on the device's orientation and the camera facing direction
remoteRotationSink?.rotation = newRotation 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) { fun handleWiredHeadsetChanged(present: Boolean) {
@ -721,7 +787,7 @@ class CallManager(
connection.setCommunicationMode() connection.setCommunicationMode()
setAudioEnabled(true) setAudioEnabled(true)
dataChannel?.let { channel -> 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) val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false)
channel.send(buffer) channel.send(buffer)
} }
@ -750,6 +816,8 @@ class CallManager(
fun isInitiator(): Boolean = peerConnection?.isInitiator() == true fun isInitiator(): Boolean = peerConnection?.isInitiator() == true
fun isCameraFrontFacing() = localCameraState.activeDirection != CameraState.Direction.BACK
interface WebRtcListener: PeerConnection.Observer { interface WebRtcListener: PeerConnection.Observer {
fun onHangup() fun onHangup()
} }

View File

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.webrtc
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel 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.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
@ -29,16 +31,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
UNTRUSTED_IDENTITY, UNTRUSTED_IDENTITY,
} }
val localRenderer: SurfaceViewRenderer? val floatingRenderer: SurfaceViewRenderer?
get() = callManager.localRenderer get() = callManager.floatingRenderer
val remoteRenderer: SurfaceViewRenderer? val fullscreenRenderer: SurfaceViewRenderer?
get() = callManager.remoteRenderer get() = callManager.fullscreenRenderer
private var _videoEnabled: Boolean = false
val videoEnabled: Boolean
get() = _videoEnabled
private var _microphoneEnabled: Boolean = true private var _microphoneEnabled: Boolean = true
@ -59,18 +56,13 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
get() = callManager.audioEvents.map { it.isEnabled } get() = callManager.audioEvents.map { it.isEnabled }
.onEach { _microphoneEnabled = it } .onEach { _microphoneEnabled = it }
val localVideoEnabledState val videoState: StateFlow<VideoState>
get() = callManager.videoEvents get() = callManager.videoState
.map { it.isEnabled }
.onEach { _videoEnabled = it }
val remoteVideoEnabledState var deviceOrientation: Orientation = Orientation.UNKNOWN
get() = callManager.remoteVideoEvents.map { it.isEnabled }
var deviceRotation: Int = 0
set(value) { set(value) {
field = value field = value
callManager.setDeviceRotation(value) callManager.setDeviceOrientation(value)
} }
val currentCallState val currentCallState
@ -85,4 +77,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val callStartTime: Long val callStartTime: Long
get() = callManager.callStartTime get() = callManager.callStartTime
fun swapVideos() {
callManager.swapVideos()
}
} }

View File

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.webrtc
enum class Orientation {
PORTRAIT,
LANDSCAPE,
REVERSED_LANDSCAPE,
UNKNOWN
}

View File

@ -41,7 +41,7 @@ class PeerConnectionWrapper(private val context: Context,
private val mediaStream: MediaStream private val mediaStream: MediaStream
private val videoSource: VideoSource? private val videoSource: VideoSource?
private val videoTrack: VideoTrack? private val videoTrack: VideoTrack?
private val rotationVideoSink = RotationVideoSink() public val rotationVideoSink = RotationVideoSink()
val readyForIce val readyForIce
get() = peerConnection?.localDescription != null && peerConnection?.remoteDescription != null get() = peerConnection?.localDescription != null && peerConnection?.remoteDescription != null
@ -103,7 +103,7 @@ class PeerConnectionWrapper(private val context: Context,
context, context,
rotationVideoSink rotationVideoSink
) )
rotationVideoSink.mirrored = newCamera.activeDirection == CameraState.Direction.FRONT
rotationVideoSink.setSink(localRenderer) rotationVideoSink.setSink(localRenderer)
newVideoTrack.setEnabled(false) newVideoTrack.setEnabled(false)
mediaStream.addTrack(newVideoTrack) mediaStream.addTrack(newVideoTrack)

View File

@ -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
}
}

View File

@ -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
}

View File

@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.webrtc.video package org.thoughtcrime.securesms.webrtc.video
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
import org.webrtc.VideoFrame import org.webrtc.VideoFrame
import org.webrtc.VideoSink import org.webrtc.VideoSink
@ -14,8 +14,7 @@ class RemoteRotationVideoProxySink: VideoSink {
val thisSink = targetSink ?: return val thisSink = targetSink ?: return
val thisFrame = frame ?: return val thisFrame = frame ?: return
val quadrantRotation = rotation.quadrantRotation() val modifiedRotation = thisFrame.rotation - rotation
val modifiedRotation = thisFrame.rotation - quadrantRotation
val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs) val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs)
thisSink.onFrame(newFrame) thisSink.onFrame(newFrame)

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.webrtc.video 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.CapturerObserver
import org.webrtc.VideoFrame import org.webrtc.VideoFrame
import org.webrtc.VideoProcessor import org.webrtc.VideoProcessor
@ -12,7 +11,6 @@ import java.util.concurrent.atomic.AtomicBoolean
class RotationVideoSink: CapturerObserver, VideoProcessor { class RotationVideoSink: CapturerObserver, VideoProcessor {
var rotation: Int = 0 var rotation: Int = 0
var mirrored = false
private val capturing = AtomicBoolean(false) private val capturing = AtomicBoolean(false)
private var capturerObserver = SoftReference<CapturerObserver>(null) private var capturerObserver = SoftReference<CapturerObserver>(null)
@ -31,13 +29,14 @@ class RotationVideoSink: CapturerObserver, VideoProcessor {
val observer = capturerObserver.get() val observer = capturerObserver.get()
if (videoFrame == null || observer == null || !capturing.get()) return if (videoFrame == null || observer == null || !capturing.get()) return
val quadrantRotation = rotation.quadrantRotation() // 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)
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)
// the frame we are sending to our contact needs to cater for rotation
observer.onFrameCaptured(newFrame) 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?) { override fun setSink(sink: VideoSink?) {

View File

@ -4,6 +4,6 @@
android:color="?android:colorControlHighlight"> android:color="?android:colorControlHighlight">
<item> <item>
<color android:color="?colorCellBackground" /> <color android:color="?colorPrimary" />
</item> </item>
</ripple> </ripple>

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="?dialog_background_color" /> <solid android:color="?backgroundSecondary" />
<corners <corners
android:topLeftRadius="@dimen/dialog_corner_radius" android:topLeftRadius="@dimen/dialog_corner_radius"

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="?attr/dialog_background_color" /> <solid android:color="?backgroundSecondary" />
<corners android:radius="?dialogCornerRadius" /> <corners android:radius="?dialogCornerRadius" />

View File

@ -6,6 +6,6 @@
android:insetBottom="16dp"> android:insetBottom="16dp">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<corners android:radius="2dp" /> <corners android:radius="2dp" />
<solid android:color="?dialog_background_color" /> <solid android:color="?backgroundSecondary" />
</shape> </shape>
</inset> </inset>

View File

@ -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>

View File

@ -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: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:strokeWidth="2"
android:fillColor="#FF3A3A" android:fillColor="#FF3A3A"
android:strokeColor="?colorPrimaryDark"/> android:strokeColor="?backgroundSecondary"/>
</vector> </vector>

View File

@ -6,7 +6,7 @@
android:bottom="@dimen/small_spacing" android:bottom="@dimen/small_spacing"
> >
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/> <solid android:color="?backgroundSecondary"/>
<corners android:bottomLeftRadius="?preferenceCornerRadius" <corners android:bottomLeftRadius="?preferenceCornerRadius"
android:bottomRightRadius="?preferenceCornerRadius"/> android:bottomRightRadius="?preferenceCornerRadius"/>
</shape> </shape>

View File

@ -4,7 +4,7 @@
<item android:left="@dimen/medium_spacing" <item android:left="@dimen/medium_spacing"
android:right="@dimen/medium_spacing"> android:right="@dimen/medium_spacing">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/> <solid android:color="?backgroundSecondary"/>
</shape> </shape>
</item> </item>
<item android:gravity="bottom" <item android:gravity="bottom"

View File

@ -6,7 +6,7 @@
android:right="@dimen/medium_spacing" android:right="@dimen/medium_spacing"
android:bottom="@dimen/small_spacing"> android:bottom="@dimen/small_spacing">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/> <solid android:color="?backgroundSecondary"/>
<corners android:radius="?preferenceCornerRadius"/> <corners android:radius="?preferenceCornerRadius"/>
</shape> </shape>
</item> </item>

View File

@ -2,7 +2,7 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/> <solid android:color="?backgroundSecondary"/>
<corners android:radius="?preferenceCornerRadius"/> <corners android:radius="?preferenceCornerRadius"/>
</shape> </shape>
</item> </item>

View File

@ -6,7 +6,7 @@
android:top="@dimen/small_spacing" android:top="@dimen/small_spacing"
> >
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/> <solid android:color="?backgroundSecondary"/>
<corners android:topLeftRadius="?preferenceCornerRadius" <corners android:topLeftRadius="?preferenceCornerRadius"
android:topRightRadius="?preferenceCornerRadius"/> android:topRightRadius="?preferenceCornerRadius"/>
</shape> </shape>

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape <shape
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="oval">
<solid android:color="@color/profile_picture_background" /> <solid android:color="@color/profile_picture_background" />
<corners android:radius="40dp" />
</shape> </shape>

View File

@ -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>

View File

@ -25,7 +25,7 @@
app:cardElevation="0dp" app:cardElevation="0dp"
app:cardCornerRadius="@dimen/dialog_corner_radius" app:cardCornerRadius="@dimen/dialog_corner_radius"
android:layout_marginHorizontal="@dimen/medium_spacing" android:layout_marginHorizontal="@dimen/medium_spacing"
app:cardBackgroundColor="?colorSettingsBackground" app:cardBackgroundColor="?backgroundSecondary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
@ -317,7 +317,7 @@
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
app:cardElevation="0dp" app:cardElevation="0dp"
android:elevation="0dp" android:elevation="0dp"
app:cardBackgroundColor="?colorSettingsBackground" app:cardBackgroundColor="?backgroundSecondary"
app:cardCornerRadius="@dimen/dialog_corner_radius" app:cardCornerRadius="@dimen/dialog_corner_radius"
android:layout_margin="@dimen/medium_spacing" android:layout_margin="@dimen/medium_spacing"
android:layout_marginBottom="@dimen/massive_spacing" android:layout_marginBottom="@dimen/massive_spacing"

View File

@ -10,7 +10,7 @@
app:layout_constraintBottom_toTopOf="@+id/unblockButton" app:layout_constraintBottom_toTopOf="@+id/unblockButton"
app:cardCornerRadius="?preferenceCornerRadius" app:cardCornerRadius="?preferenceCornerRadius"
app:cardElevation="0dp" app:cardElevation="0dp"
app:cardBackgroundColor="?colorSettingsBackground" app:cardBackgroundColor="?backgroundSecondary"
android:layout_marginHorizontal="@dimen/medium_spacing" android:layout_marginHorizontal="@dimen/medium_spacing"
android:layout_marginVertical="@dimen/large_spacing" android:layout_marginVertical="@dimen/large_spacing"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -8,7 +8,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<FrameLayout <FrameLayout
android:id="@+id/remote_parent" android:id="@+id/fullscreen_renderer_container"
android:background="@color/black" android:background="@color/black"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -18,20 +18,32 @@
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<FrameLayout <FrameLayout
android:id="@+id/remote_renderer" android:id="@+id/fullscreen_renderer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_gravity="center"/> android:layout_gravity="center"/>
</FrameLayout> </FrameLayout>
<ImageView
android:id="@+id/remote_recipient" <org.thoughtcrime.securesms.components.ProfilePictureView
app:layout_constraintStart_toStartOf="@id/remote_parent" android:id="@+id/userAvatar"
app:layout_constraintEnd_toEndOf="@id/remote_parent" app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container"
app:layout_constraintTop_toTopOf="@id/remote_parent" app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container"
app:layout_constraintBottom_toBottomOf="@id/remote_parent" app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container"
app:layout_constraintBottom_toBottomOf="@id/fullscreen_renderer_container"
app:layout_constraintVertical_bias="0.4" app:layout_constraintVertical_bias="0.4"
android:layout_width="@dimen/extra_large_profile_picture_size" 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 <ImageView
android:id="@+id/back_arrow" android:id="@+id/back_arrow"
@ -71,9 +83,9 @@
android:foregroundGravity="center" android:foregroundGravity="center"
android:visibility="gone" android:visibility="gone"
app:SpinKit_Color="@color/core_white" app:SpinKit_Color="@color/core_white"
app:layout_constraintEnd_toEndOf="@+id/remote_recipient" app:layout_constraintEnd_toEndOf="@+id/contactAvatar"
app:layout_constraintStart_toStartOf="@+id/remote_recipient" app:layout_constraintStart_toStartOf="@+id/contactAvatar"
app:layout_constraintTop_toBottomOf="@id/remote_recipient" app:layout_constraintTop_toBottomOf="@id/contactAvatar"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView
@ -111,6 +123,7 @@
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<FrameLayout <FrameLayout
android:id="@+id/floating_renderer_container"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintDimensionRatio="h,9:16" app:layout_constraintDimensionRatio="h,9:16"
@ -118,12 +131,21 @@
android:layout_marginVertical="@dimen/massive_spacing" android:layout_marginVertical="@dimen/massive_spacing"
app:layout_constraintWidth_percent="0.2" app:layout_constraintWidth_percent="0.2"
android:layout_height="0dp" 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 <FrameLayout
android:elevation="8dp" android:elevation="8dp"
android:id="@+id/local_renderer" android:id="@+id/floating_renderer"
android:layout_width="match_parent" 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 <com.github.ybq.android.spinkit.SpinKitView
android:id="@+id/local_loading_view" android:id="@+id/local_loading_view"
style="@style/SpinKitView.Large.ThreeBounce" style="@style/SpinKitView.Large.ThreeBounce"
@ -133,8 +155,20 @@
android:layout_gravity="center" android:layout_gravity="center"
tools:visibility="visible" tools:visibility="visible"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </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 <ImageView
android:id="@+id/endCallButton" android:id="@+id/endCallButton"
android:background="@drawable/circle_tintable" android:background="@drawable/circle_tintable"

View File

@ -10,7 +10,7 @@
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:chipStrokeWidth="1dp" app:chipStrokeWidth="1dp"
app:chipStrokeColor="?elementBorderColor" app:chipStrokeColor="?elementBorderColor"
app:chipBackgroundColor="?dialog_background_color" app:chipBackgroundColor="?backgroundSecondary"
app:chipMinTouchTargetSize="0dp" app:chipMinTouchTargetSize="0dp"
app:chipStartPadding="4dp" app:chipStartPadding="4dp"
tools:ignore="TouchTargetSizeCheck" tools:ignore="TouchTargetSizeCheck"

View File

@ -85,8 +85,7 @@
android:textColor="?unreadIndicatorTextColor" android:textColor="?unreadIndicatorTextColor"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textStyle="bold" android:textStyle="bold"
tools:text="8" tools:text="8"/>
tools:textColor="?android:textColorPrimary" />
</RelativeLayout> </RelativeLayout>
@ -115,8 +114,7 @@
android:textColor="?unreadIndicatorTextColor" android:textColor="?unreadIndicatorTextColor"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textStyle="bold" android:textStyle="bold"
android:text="@" android:text="@" />
tools:textColor="?android:textColorPrimary" />
</RelativeLayout> </RelativeLayout>

View File

@ -27,17 +27,11 @@
</RelativeLayout> </RelativeLayout>
<ImageView <ImageView
android:scaleType="centerCrop"
android:id="@+id/singleModeImageView" 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:scaleType="centerCrop"
android:layout_width="@dimen/large_profile_picture_size" android:layout_width="match_parent"
android:layout_height="@dimen/large_profile_picture_size" android:layout_height="match_parent"
android:background="@drawable/profile_picture_view_large_background" /> android:adjustViewBounds="true"
android:background="@drawable/profile_picture_view_background" />
</merge> </merge>

View File

@ -61,7 +61,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="8dp" android:padding="8dp"
android:background="?colorPrimaryDark" android:background="?backgroundSecondary"
app:SpinKit_Color="?android:textColorPrimary" app:SpinKit_Color="?android:textColorPrimary"
android:visibility="gone"/> android:visibility="gone"/>

View File

@ -186,9 +186,6 @@
<string name="MediaOverviewActivity_Select_all">أختر الكل</string> <string name="MediaOverviewActivity_Select_all">أختر الكل</string>
<string name="MediaOverviewActivity_collecting_attachments">جارٍ جمع المرفقات...</string> <string name="MediaOverviewActivity_collecting_attachments">جارٍ جمع المرفقات...</string>
<!-- NotificationMmsMessageRecord --> <!-- NotificationMmsMessageRecord -->
<string name="NotificationMmsMessageRecord_multimedia_message">رسالة وسائط متعددة</string>
<string name="NotificationMmsMessageRecord_downloading_mms_message">تنزيل رسالة الوسائط المتعددة</string>
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">خطأ في تنزيل رسالة الوسائط المتعددة، انقر لاعادة المحاولة</string>
<!-- MediaPickerActivity --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">إرسال إلى %s</string> <string name="MediaPickerActivity_send_to">إرسال إلى %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -186,9 +186,6 @@
<string name="MediaOverviewActivity_Select_all">أختر الكل</string> <string name="MediaOverviewActivity_Select_all">أختر الكل</string>
<string name="MediaOverviewActivity_collecting_attachments">جارٍ جمع المرفقات...</string> <string name="MediaOverviewActivity_collecting_attachments">جارٍ جمع المرفقات...</string>
<!-- NotificationMmsMessageRecord --> <!-- NotificationMmsMessageRecord -->
<string name="NotificationMmsMessageRecord_multimedia_message">رسالة وسائط متعددة</string>
<string name="NotificationMmsMessageRecord_downloading_mms_message">تنزيل رسالة الوسائط المتعددة</string>
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">خطأ في تنزيل رسالة الوسائط المتعددة، انقر لاعادة المحاولة</string>
<!-- MediaPickerActivity --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">إرسال إلى %s</string> <string name="MediaPickerActivity_send_to">إرسال إلى %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -150,9 +150,6 @@
<string name="MediaOverviewActivity_Select_all">Hamısını seç</string> <string name="MediaOverviewActivity_Select_all">Hamısını seç</string>
<string name="MediaOverviewActivity_collecting_attachments">Qoşmalar yığılır...</string> <string name="MediaOverviewActivity_collecting_attachments">Qoşmalar yığılır...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">%s - göndər</string> <string name="MediaPickerActivity_send_to">%s - göndər</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -150,9 +150,6 @@
<string name="MediaOverviewActivity_Select_all">Hamısını seç</string> <string name="MediaOverviewActivity_Select_all">Hamısını seç</string>
<string name="MediaOverviewActivity_collecting_attachments">Qoşmalar yığılır...</string> <string name="MediaOverviewActivity_collecting_attachments">Qoşmalar yığılır...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">%s - göndər</string> <string name="MediaPickerActivity_send_to">%s - göndər</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -96,9 +96,6 @@
<string name="MediaOverviewActivity_Select_all">Pilih semua</string> <string name="MediaOverviewActivity_Select_all">Pilih semua</string>
<string name="MediaOverviewActivity_collecting_attachments">Mengumpulkan lampiran...</string> <string name="MediaOverviewActivity_collecting_attachments">Mengumpulkan lampiran...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Kirim ke %s</string> <string name="MediaPickerActivity_send_to">Kirim ke %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -96,9 +96,6 @@
<string name="MediaOverviewActivity_Select_all">Pilih semua</string> <string name="MediaOverviewActivity_Select_all">Pilih semua</string>
<string name="MediaOverviewActivity_collecting_attachments">Mengumpulkan lampiran...</string> <string name="MediaOverviewActivity_collecting_attachments">Mengumpulkan lampiran...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Kirim ke %s</string> <string name="MediaPickerActivity_send_to">Kirim ke %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -142,9 +142,6 @@
<string name="MediaOverviewActivity_Select_all">Избери всичко</string> <string name="MediaOverviewActivity_Select_all">Избери всичко</string>
<string name="MediaOverviewActivity_collecting_attachments">Събиране на прикачени файлове...</string> <string name="MediaOverviewActivity_collecting_attachments">Събиране на прикачени файлове...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Изпрати на %s</string> <string name="MediaPickerActivity_send_to">Изпрати на %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -142,9 +142,6 @@
<string name="MediaOverviewActivity_Select_all">Избери всичко</string> <string name="MediaOverviewActivity_Select_all">Избери всичко</string>
<string name="MediaOverviewActivity_collecting_attachments">Събиране на прикачени файлове...</string> <string name="MediaOverviewActivity_collecting_attachments">Събиране на прикачени файлове...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Изпрати на %s</string> <string name="MediaPickerActivity_send_to">Изпрати на %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -147,9 +147,6 @@
<string name="MediaOverviewActivity_Select_all">Selecciona-ho tot</string> <string name="MediaOverviewActivity_Select_all">Selecciona-ho tot</string>
<string name="MediaOverviewActivity_collecting_attachments">S\'estan adjuntant els fitxers...</string> <string name="MediaOverviewActivity_collecting_attachments">S\'estan adjuntant els fitxers...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Envia-ho a %s</string> <string name="MediaPickerActivity_send_to">Envia-ho a %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -147,9 +147,6 @@
<string name="MediaOverviewActivity_Select_all">Selecciona-ho tot</string> <string name="MediaOverviewActivity_Select_all">Selecciona-ho tot</string>
<string name="MediaOverviewActivity_collecting_attachments">S\'estan adjuntant els fitxers...</string> <string name="MediaOverviewActivity_collecting_attachments">S\'estan adjuntant els fitxers...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Envia-ho a %s</string> <string name="MediaPickerActivity_send_to">Envia-ho a %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -168,9 +168,6 @@
<string name="MediaOverviewActivity_Select_all">Označit vše</string> <string name="MediaOverviewActivity_Select_all">Označit vše</string>
<string name="MediaOverviewActivity_collecting_attachments">Shromažďuji přílohy...</string> <string name="MediaOverviewActivity_collecting_attachments">Shromažďuji přílohy...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Poslat %s</string> <string name="MediaPickerActivity_send_to">Poslat %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -168,9 +168,6 @@
<string name="MediaOverviewActivity_Select_all">Označit vše</string> <string name="MediaOverviewActivity_Select_all">Označit vše</string>
<string name="MediaOverviewActivity_collecting_attachments">Shromažďuji přílohy...</string> <string name="MediaOverviewActivity_collecting_attachments">Shromažďuji přílohy...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Poslat %s</string> <string name="MediaPickerActivity_send_to">Poslat %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -172,9 +172,6 @@
<string name="MediaOverviewActivity_Select_all">Dewis popeth</string> <string name="MediaOverviewActivity_Select_all">Dewis popeth</string>
<string name="MediaOverviewActivity_collecting_attachments">Casglu atodiadau...</string> <string name="MediaOverviewActivity_collecting_attachments">Casglu atodiadau...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Anfon i %s</string> <string name="MediaPickerActivity_send_to">Anfon i %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -172,9 +172,6 @@
<string name="MediaOverviewActivity_Select_all">Dewis popeth</string> <string name="MediaOverviewActivity_Select_all">Dewis popeth</string>
<string name="MediaOverviewActivity_collecting_attachments">Casglu atodiadau...</string> <string name="MediaOverviewActivity_collecting_attachments">Casglu atodiadau...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Anfon i %s</string> <string name="MediaPickerActivity_send_to">Anfon i %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -150,9 +150,6 @@
<string name="MediaOverviewActivity_Select_all">Vælg alle</string> <string name="MediaOverviewActivity_Select_all">Vælg alle</string>
<string name="MediaOverviewActivity_collecting_attachments">Samler vedhæftninger...</string> <string name="MediaOverviewActivity_collecting_attachments">Samler vedhæftninger...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Send til %s</string> <string name="MediaPickerActivity_send_to">Send til %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -150,9 +150,6 @@
<string name="MediaOverviewActivity_Select_all">Vælg alle</string> <string name="MediaOverviewActivity_Select_all">Vælg alle</string>
<string name="MediaOverviewActivity_collecting_attachments">Samler vedhæftninger...</string> <string name="MediaOverviewActivity_collecting_attachments">Samler vedhæftninger...</string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">Send til %s</string> <string name="MediaPickerActivity_send_to">Send til %s</string>
<!-- MediaSendActivity --> <!-- MediaSendActivity -->

View File

@ -150,9 +150,6 @@
<string name="MediaOverviewActivity_Select_all">Alle auswählen</string> <string name="MediaOverviewActivity_Select_all">Alle auswählen</string>
<string name="MediaOverviewActivity_collecting_attachments">Anhänge werden gesammelt </string> <string name="MediaOverviewActivity_collecting_attachments">Anhänge werden gesammelt </string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">An %s senden</string> <string name="MediaPickerActivity_send_to">An %s senden</string>
<!-- MediaSendActivity --> <!-- 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="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_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_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_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="fragment_scan_qr_code_grant_camera_access_button_title">Kamerazugriff gewähren</string>
<string name="activity_create_closed_group_title">Neue geschlossene Gruppe</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_day">Tag</string>
<string name="dialog_ui_mode_option_night">Nacht</string> <string name="dialog_ui_mode_option_night">Nacht</string>
<string name="dialog_ui_mode_option_system_default">Systemstandard</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">Anhang</string>
<string name="attachment_type_voice_message">Sprachnachricht</string> <string name="attachment_type_voice_message">Sprachnachricht</string>
<string name="details">Details</string> <string name="details">Details</string>

View File

@ -150,9 +150,6 @@
<string name="MediaOverviewActivity_Select_all">Alle auswählen</string> <string name="MediaOverviewActivity_Select_all">Alle auswählen</string>
<string name="MediaOverviewActivity_collecting_attachments">Anhänge werden gesammelt </string> <string name="MediaOverviewActivity_collecting_attachments">Anhänge werden gesammelt </string>
<!-- NotificationMmsMessageRecord --> <!-- 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 --> <!-- MediaPickerActivity -->
<string name="MediaPickerActivity_send_to">An %s senden</string> <string name="MediaPickerActivity_send_to">An %s senden</string>
<!-- MediaSendActivity --> <!-- 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="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_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_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_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="fragment_scan_qr_code_grant_camera_access_button_title">Kamerazugriff gewähren</string>
<string name="activity_create_closed_group_title">Neue geschlossene Gruppe</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_day">Tag</string>
<string name="dialog_ui_mode_option_night">Nacht</string> <string name="dialog_ui_mode_option_night">Nacht</string>
<string name="dialog_ui_mode_option_system_default">Systemstandard</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">Anhang</string>
<string name="attachment_type_voice_message">Sprachnachricht</string> <string name="attachment_type_voice_message">Sprachnachricht</string>
<string name="details">Details</string> <string name="details">Details</string>

Some files were not shown because too many files have changed in this diff Show More