@ -38,7 +38,8 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/
|
|||||||
pull: 'always',
|
pull: 'always',
|
||||||
environment: { ANDROID_HOME: '/usr/lib/android-sdk' },
|
environment: { ANDROID_HOME: '/usr/lib/android-sdk' },
|
||||||
commands: [
|
commands: [
|
||||||
'apt-get install -y ninja-build',
|
'apt-get install -y ninja-build openjdk-17-jdk',
|
||||||
|
'update-java-alternatives -s java-1.17.0-openjdk-amd64',
|
||||||
'./gradlew testPlayDebugUnitTestCoverageReport'
|
'./gradlew testPlayDebugUnitTestCoverageReport'
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -78,7 +79,8 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/
|
|||||||
pull: 'always',
|
pull: 'always',
|
||||||
environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, ANDROID_HOME: '/usr/lib/android-sdk' },
|
environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, ANDROID_HOME: '/usr/lib/android-sdk' },
|
||||||
commands: [
|
commands: [
|
||||||
'apt-get install -y ninja-build',
|
'apt-get install -y ninja-build openjdk-17-jdk',
|
||||||
|
'update-java-alternatives -s java-1.17.0-openjdk-amd64',
|
||||||
'./gradlew assemblePlayDebug',
|
'./gradlew assemblePlayDebug',
|
||||||
'./scripts/drone-static-upload.sh'
|
'./scripts/drone-static-upload.sh'
|
||||||
],
|
],
|
||||||
|
@ -13,8 +13,8 @@ configurations.forEach {
|
|||||||
it.exclude module: "commons-logging"
|
it.exclude module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 380
|
def canonicalVersionCode = 382
|
||||||
def canonicalVersionName = "1.19.2"
|
def canonicalVersionName = "1.20.0"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
@ -310,7 +310,6 @@ dependencies {
|
|||||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||||
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
||||||
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
|
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
|
||||||
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
|
|
||||||
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
||||||
implementation "com.opencsv:opencsv:4.6"
|
implementation "com.opencsv:opencsv:4.6"
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
@ -366,7 +365,7 @@ dependencies {
|
|||||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$composeVersion"
|
debugImplementation "androidx.compose.ui:ui-test-manifest:$composeVersion"
|
||||||
|
|
||||||
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
||||||
implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha"
|
implementation "com.google.accompanist:accompanist-permissions:0.36.0"
|
||||||
implementation "com.google.accompanist:accompanist-drawablepainter:0.33.1-alpha"
|
implementation "com.google.accompanist:accompanist-drawablepainter:0.33.1-alpha"
|
||||||
|
|
||||||
implementation "androidx.camera:camera-camera2:1.3.2"
|
implementation "androidx.camera:camera-camera2:1.3.2"
|
||||||
|
@ -457,39 +457,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void resubmitProfilePictureIfNeeded() {
|
private void resubmitProfilePictureIfNeeded() {
|
||||||
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
|
ProfilePictureUtilities.INSTANCE.resubmitProfilePictureIfNeeded(this);
|
||||||
// at a certain interval to ensure it's always available.
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey == null) return;
|
|
||||||
long now = new Date().getTime();
|
|
||||||
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
|
|
||||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
|
||||||
ThreadUtils.queue(() -> {
|
|
||||||
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
|
||||||
Log.d("Loki-Avatar", "Uploading Avatar Started");
|
|
||||||
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
|
||||||
try {
|
|
||||||
// Read the file into a byte array
|
|
||||||
InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey));
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
int count;
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
|
||||||
baos.write(buffer, 0, count);
|
|
||||||
}
|
|
||||||
baos.flush();
|
|
||||||
byte[] profilePicture = baos.toByteArray();
|
|
||||||
// Re-upload it
|
|
||||||
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
|
|
||||||
// Update the last profile picture upload date
|
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
|
|
||||||
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("Loki-Avatar", "Uploading avatar failed.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadEmojiSearchIndexIfNeeded() {
|
private void loadEmojiSearchIndexIfNeeded() {
|
||||||
|
@ -412,14 +412,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||||
.withPermanentDenialDialog(Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
|
.withPermanentDenialDialog(getPermanentlyDeniedStorageText())
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
|
||||||
.format().toString())
|
|
||||||
.onAnyDenied(() -> {
|
.onAnyDenied(() -> {
|
||||||
String txt = Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
|
Toast.makeText(this, getPermanentlyDeniedStorageText(), Toast.LENGTH_LONG).show();
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
|
||||||
.format().toString();
|
|
||||||
Toast.makeText(this, txt, Toast.LENGTH_LONG).show();
|
|
||||||
})
|
})
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||||
@ -436,6 +431,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getPermanentlyDeniedStorageText(){
|
||||||
|
return Phrase.from(getApplicationContext(), R.string.permissionsStorageDeniedLegacy)
|
||||||
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
}
|
||||||
|
|
||||||
private void sendMediaSavedNotificationIfNeeded() {
|
private void sendMediaSavedNotificationIfNeeded() {
|
||||||
if (conversationRecipient.isGroupRecipient()) return;
|
if (conversationRecipient.isGroupRecipient()) return;
|
||||||
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
|
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
|
import org.thoughtcrime.securesms.permissions.SettingsDialog
|
||||||
|
|
||||||
|
class MissingMicrophonePermissionDialog {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(context: Context) = SettingsDialog.show(
|
||||||
|
context,
|
||||||
|
Phrase.from(context, R.string.permissionsMicrophoneAccessRequired)
|
||||||
|
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,10 @@ fun showMuteDialog(
|
|||||||
if (entry.stringRes == R.string.notificationsMute) {
|
if (entry.stringRes == R.string.notificationsMute) {
|
||||||
context.getString(R.string.notificationsMute)
|
context.getString(R.string.notificationsMute)
|
||||||
} else {
|
} else {
|
||||||
val largeTimeUnitString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, Option.entries[index].getTime().milliseconds)
|
val largeTimeUnitString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(
|
||||||
|
context,
|
||||||
|
Option.entries[index].duration.milliseconds
|
||||||
|
)
|
||||||
context.getSubbedString(entry.stringRes, TIME_LARGE_KEY to largeTimeUnitString)
|
context.getSubbedString(entry.stringRes, TIME_LARGE_KEY to largeTimeUnitString)
|
||||||
}
|
}
|
||||||
}.toTypedArray()) {
|
}.toTypedArray()) {
|
||||||
@ -33,16 +36,17 @@ fun showMuteDialog(
|
|||||||
// less than the entire duration - so 1 hour becomes 59 minutes, 1 day becomes 23 hours etc.
|
// less than the entire duration - so 1 hour becomes 59 minutes, 1 day becomes 23 hours etc.
|
||||||
// As we really want to see the actual set time (1 hour / 1 day etc.) then we'll bump it by
|
// As we really want to see the actual set time (1 hour / 1 day etc.) then we'll bump it by
|
||||||
// 1 second which is neither here nor there in the grand scheme of things.
|
// 1 second which is neither here nor there in the grand scheme of things.
|
||||||
onMuteDuration(Option.entries[it].getTime() + System.currentTimeMillis() + 1.seconds.inWholeMilliseconds)
|
val muteTime = Option.entries[it].duration
|
||||||
|
val muteTimeFromNow = if (muteTime == Long.MAX_VALUE) muteTime
|
||||||
|
else muteTime + System.currentTimeMillis() + 1.seconds.inWholeMilliseconds
|
||||||
|
onMuteDuration(muteTimeFromNow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
|
private enum class Option(@StringRes val stringRes: Int, val duration: Long) {
|
||||||
ONE_HOUR(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(1)),
|
ONE_HOUR(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(1)),
|
||||||
TWO_HOURS(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(2)),
|
TWO_HOURS(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(2)),
|
||||||
ONE_DAY(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(1)),
|
ONE_DAY(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(1)),
|
||||||
SEVEN_DAYS(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(7)),
|
SEVEN_DAYS(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(7)),
|
||||||
FOREVER(R.string.notificationsMute, getTime = { Long.MAX_VALUE } );
|
FOREVER(R.string.notificationsMute, duration = Long.MAX_VALUE );
|
||||||
|
|
||||||
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { duration } )
|
|
||||||
}
|
}
|
@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@ -29,14 +31,21 @@ import android.provider.OpenableColumns;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
|
import com.squareup.phrase.Phrase;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.DistributionTypes;
|
import org.session.libsession.utilities.DistributionTypes;
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
@ -57,8 +66,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
|||||||
* @author Jake McGinty
|
* @author Jake McGinty
|
||||||
*/
|
*/
|
||||||
public class ShareActivity extends PassphraseRequiredActionBarActivity
|
public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||||
implements ContactSelectionListFragment.OnContactSelectedListener
|
implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||||
{
|
|
||||||
private static final String TAG = ShareActivity.class.getSimpleName();
|
private static final String TAG = ShareActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String EXTRA_THREAD_ID = "thread_id";
|
public static final String EXTRA_THREAD_ID = "thread_id";
|
||||||
@ -84,7 +92,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
setContentView(R.layout.share_activity);
|
setContentView(R.layout.share_activity);
|
||||||
|
|
||||||
// initializeToolbar();
|
initializeToolbar();
|
||||||
initializeResources();
|
initializeResources();
|
||||||
initializeSearch();
|
initializeSearch();
|
||||||
initializeMedia();
|
initializeMedia();
|
||||||
@ -126,13 +134,14 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
|||||||
else super.onBackPressed();
|
else super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* private void initializeToolbar() {
|
private void initializeToolbar() {
|
||||||
SearchToolbar toolbar = findViewById(R.id.search_toolbar);
|
TextView tootlbarTitle = findViewById(R.id.title);
|
||||||
setSupportActionBar(toolbar);
|
tootlbarTitle.setText(
|
||||||
ActionBar actionBar = getSupportActionBar();
|
Phrase.from(getApplicationContext(), R.string.shareToSession)
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
actionBar.setHomeButtonEnabled(true);
|
.format().toString()
|
||||||
}*/
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
progressWheel = findViewById(R.id.progress_wheel);
|
progressWheel = findViewById(R.id.progress_wheel);
|
||||||
|
@ -74,8 +74,9 @@ class AvatarSelection(
|
|||||||
*/
|
*/
|
||||||
fun startAvatarSelection(
|
fun startAvatarSelection(
|
||||||
includeClear: Boolean,
|
includeClear: Boolean,
|
||||||
attemptToIncludeCamera: Boolean
|
attemptToIncludeCamera: Boolean,
|
||||||
): File? {
|
createTempFile: ()->File?
|
||||||
|
) {
|
||||||
var captureFile: File? = null
|
var captureFile: File? = null
|
||||||
val hasCameraPermission = ContextCompat
|
val hasCameraPermission = ContextCompat
|
||||||
.checkSelfPermission(
|
.checkSelfPermission(
|
||||||
@ -83,18 +84,11 @@ class AvatarSelection(
|
|||||||
Manifest.permission.CAMERA
|
Manifest.permission.CAMERA
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
if (attemptToIncludeCamera && hasCameraPermission) {
|
if (attemptToIncludeCamera && hasCameraPermission) {
|
||||||
try {
|
captureFile = createTempFile()
|
||||||
captureFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(activity))
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e("Cannot reserve a temporary avatar capture file.", e)
|
|
||||||
} catch (e: NoExternalStorageException) {
|
|
||||||
Log.e("Cannot reserve a temporary avatar capture file.", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear)
|
val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear)
|
||||||
onPickImage.launch(chooserIntent)
|
onPickImage.launch(chooserIntent)
|
||||||
return captureFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAvatarSelectionIntent(
|
private fun createAvatarSelectionIntent(
|
||||||
|
@ -89,9 +89,9 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if (intent?.action == ACTION_ANSWER) {
|
if (intent.action == ACTION_ANSWER) {
|
||||||
val answerIntent = WebRtcCallService.acceptCallIntent(this)
|
val answerIntent = WebRtcCallService.acceptCallIntent(this)
|
||||||
answerIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
answerIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||||
ContextCompat.startForegroundService(this, answerIntent)
|
ContextCompat.startForegroundService(this, answerIntent)
|
||||||
|
@ -102,14 +102,13 @@ class ConversationActionBarView @JvmOverloads constructor(
|
|||||||
if (config?.isEnabled == true) {
|
if (config?.isEnabled == true) {
|
||||||
// Get the type of disappearing message and the abbreviated duration..
|
// Get the type of disappearing message and the abbreviated duration..
|
||||||
val dmTypeString = when (config.expiryMode) {
|
val dmTypeString = when (config.expiryMode) {
|
||||||
is AfterRead -> context.getString(R.string.read)
|
is AfterRead -> R.string.disappearingMessagesDisappearAfterReadState
|
||||||
else -> context.getString(R.string.send)
|
else -> R.string.disappearingMessagesDisappearAfterSendState
|
||||||
}
|
}
|
||||||
val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(config.expiryMode.expirySeconds)
|
val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(config.expiryMode.expirySeconds)
|
||||||
|
|
||||||
// ..then substitute into the string..
|
// ..then substitute into the string..
|
||||||
val subtitleTxt = context.getSubbedString(R.string.disappearingMessagesDisappear,
|
val subtitleTxt = context.getSubbedString(dmTypeString,
|
||||||
DISAPPEARING_MESSAGES_TYPE_KEY to dmTypeString,
|
|
||||||
TIME_KEY to durationAbbreviated
|
TIME_KEY to durationAbbreviated
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -126,9 +125,7 @@ class ConversationActionBarView @JvmOverloads constructor(
|
|||||||
settings += ConversationSetting(
|
settings += ConversationSetting(
|
||||||
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
|
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
|
||||||
?.let {
|
?.let {
|
||||||
val mutedDuration = (it - System.currentTimeMillis()).milliseconds
|
context.getString(R.string.notificationsHeaderMute)
|
||||||
val durationString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, mutedDuration)
|
|
||||||
context.getSubbedString(R.string.notificationsMuteFor, TIME_LARGE_KEY to durationString)
|
|
||||||
}
|
}
|
||||||
?: context.getString(R.string.notificationsMuted),
|
?: context.getString(R.string.notificationsMuted),
|
||||||
ConversationSettingType.NOTIFICATION,
|
ConversationSettingType.NOTIFICATION,
|
||||||
|
@ -58,7 +58,7 @@ class DisappearingMessagesViewModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode?.maybeConvertToLegacy(isNewConfigEnabled) ?: ExpiryMode.NONE
|
val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode ?: ExpiryMode.NONE
|
||||||
val recipient = threadDb.getRecipientForThreadId(threadId)
|
val recipient = threadDb.getRecipientForThreadId(threadId)
|
||||||
val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient }
|
val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient }
|
||||||
?.run { groupDb.getGroup(address.toGroupString()).orNull() }
|
?.run { groupDb.getGroup(address.toGroupString()).orNull() }
|
||||||
@ -80,7 +80,7 @@ class DisappearingMessagesViewModel(
|
|||||||
|
|
||||||
override fun onSetClick() = viewModelScope.launch {
|
override fun onSetClick() = viewModelScope.launch {
|
||||||
val state = _state.value
|
val state = _state.value
|
||||||
val mode = state.expiryMode?.coerceLegacyToAfterSend()
|
val mode = state.expiryMode
|
||||||
val address = state.address
|
val address = state.address
|
||||||
if (address == null || mode == null) {
|
if (address == null || mode == null) {
|
||||||
_event.send(Event.FAIL)
|
_event.send(Event.FAIL)
|
||||||
@ -92,8 +92,6 @@ class DisappearingMessagesViewModel(
|
|||||||
_event.send(Event.SUCCESS)
|
_event.send(Event.SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ExpiryMode.coerceLegacyToAfterSend() = takeUnless { it is ExpiryMode.Legacy } ?: ExpiryMode.AfterSend(expirySeconds)
|
|
||||||
|
|
||||||
@dagger.assisted.AssistedFactory
|
@dagger.assisted.AssistedFactory
|
||||||
interface AssistedFactory {
|
interface AssistedFactory {
|
||||||
fun create(threadId: Long): Factory
|
fun create(threadId: Long): Factory
|
||||||
@ -125,5 +123,3 @@ class DisappearingMessagesViewModel(
|
|||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ExpiryMode.maybeConvertToLegacy(isNewConfigEnabled: Boolean): ExpiryMode = takeIf { isNewConfigEnabled } ?: ExpiryMode.Legacy(expirySeconds)
|
|
||||||
|
@ -32,14 +32,13 @@ data class State(
|
|||||||
|
|
||||||
val nextType get() = when {
|
val nextType get() = when {
|
||||||
expiryType == ExpiryType.AFTER_READ -> ExpiryType.AFTER_READ
|
expiryType == ExpiryType.AFTER_READ -> ExpiryType.AFTER_READ
|
||||||
isNewConfigEnabled -> ExpiryType.AFTER_SEND
|
else -> ExpiryType.AFTER_SEND
|
||||||
else -> ExpiryType.LEGACY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val duration get() = expiryMode?.duration
|
val duration get() = expiryMode?.duration
|
||||||
val expiryType get() = expiryMode?.type
|
val expiryType get() = expiryMode?.type
|
||||||
|
|
||||||
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
|
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && isNewConfigEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -54,11 +53,6 @@ enum class ExpiryType(
|
|||||||
R.string.off,
|
R.string.off,
|
||||||
contentDescription = R.string.AccessibilityId_disappearingMessagesOff,
|
contentDescription = R.string.AccessibilityId_disappearingMessagesOff,
|
||||||
),
|
),
|
||||||
LEGACY(
|
|
||||||
ExpiryMode::Legacy,
|
|
||||||
R.string.expiration_type_disappear_legacy,
|
|
||||||
contentDescription = R.string.AccessibilityId_disappearingMessagesLegacy
|
|
||||||
),
|
|
||||||
AFTER_READ(
|
AFTER_READ(
|
||||||
ExpiryMode::AfterRead,
|
ExpiryMode::AfterRead,
|
||||||
R.string.disappearingMessagesDisappearAfterRead,
|
R.string.disappearingMessagesDisappearAfterRead,
|
||||||
@ -83,7 +77,6 @@ enum class ExpiryType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ExpiryMode.type: ExpiryType get() = when(this) {
|
val ExpiryMode.type: ExpiryType get() = when(this) {
|
||||||
is ExpiryMode.Legacy -> ExpiryType.LEGACY
|
|
||||||
is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
|
is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
|
||||||
is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
|
is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
|
||||||
else -> ExpiryType.NONE
|
else -> ExpiryType.NONE
|
||||||
|
@ -23,7 +23,6 @@ fun State.toUiState() = UiState(
|
|||||||
private fun State.typeOptions(): List<ExpiryRadioOption>? = if (typeOptionsHidden) null else {
|
private fun State.typeOptions(): List<ExpiryRadioOption>? = if (typeOptionsHidden) null else {
|
||||||
buildList {
|
buildList {
|
||||||
add(offTypeOption())
|
add(offTypeOption())
|
||||||
if (!isNewConfigEnabled) add(legacyTypeOption())
|
|
||||||
if (!isGroup) add(afterReadTypeOption())
|
if (!isGroup) add(afterReadTypeOption())
|
||||||
add(afterSendTypeOption())
|
add(afterSendTypeOption())
|
||||||
}
|
}
|
||||||
@ -48,7 +47,6 @@ private fun State.timeOptions(): List<ExpiryRadioOption>? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun State.offTypeOption() = typeOption(ExpiryType.NONE)
|
private fun State.offTypeOption() = typeOption(ExpiryType.NONE)
|
||||||
private fun State.legacyTypeOption() = typeOption(ExpiryType.LEGACY)
|
|
||||||
private fun State.afterReadTypeOption() = newTypeOption(ExpiryType.AFTER_READ)
|
private fun State.afterReadTypeOption() = newTypeOption(ExpiryType.AFTER_READ)
|
||||||
private fun State.afterSendTypeOption() = newTypeOption(ExpiryType.AFTER_SEND)
|
private fun State.afterSendTypeOption() = newTypeOption(ExpiryType.AFTER_SEND)
|
||||||
private fun State.newTypeOption(type: ExpiryType) = typeOption(type, isNewConfigEnabled && isSelfAdmin)
|
private fun State.newTypeOption(type: ExpiryType) = typeOption(type, isNewConfigEnabled && isSelfAdmin)
|
||||||
|
@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.ui.Callbacks
|
|||||||
import org.thoughtcrime.securesms.ui.NoOpCallbacks
|
import org.thoughtcrime.securesms.ui.NoOpCallbacks
|
||||||
import org.thoughtcrime.securesms.ui.OptionsCard
|
import org.thoughtcrime.securesms.ui.OptionsCard
|
||||||
import org.thoughtcrime.securesms.ui.RadioOption
|
import org.thoughtcrime.securesms.ui.RadioOption
|
||||||
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import org.thoughtcrime.securesms.ui.fadingEdges
|
import org.thoughtcrime.securesms.ui.fadingEdges
|
||||||
@ -71,7 +72,8 @@ fun DisappearingMessages(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showSetButton) SlimOutlineButton(
|
if (state.showSetButton){
|
||||||
|
PrimaryOutlineButton(
|
||||||
stringResource(R.string.set),
|
stringResource(R.string.set),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.contentDescription(R.string.AccessibilityId_setButton)
|
.contentDescription(R.string.AccessibilityId_setButton)
|
||||||
@ -81,3 +83,4 @@ fun DisappearingMessages(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -27,21 +27,18 @@ fun PreviewStates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class StatePreviewParameterProvider : PreviewParameterProvider<State> {
|
class StatePreviewParameterProvider : PreviewParameterProvider<State> {
|
||||||
override val values = newConfigValues.filter { it.expiryType != ExpiryType.LEGACY } + newConfigValues.map { it.copy(isNewConfigEnabled = false) }
|
override val values = newConfigValues + newConfigValues.map { it.copy(isNewConfigEnabled = false) }
|
||||||
|
|
||||||
private val newConfigValues get() = sequenceOf(
|
private val newConfigValues get() = sequenceOf(
|
||||||
// new 1-1
|
// new 1-1
|
||||||
State(expiryMode = ExpiryMode.NONE),
|
State(expiryMode = ExpiryMode.NONE),
|
||||||
State(expiryMode = ExpiryMode.Legacy(43200)),
|
|
||||||
State(expiryMode = ExpiryMode.AfterRead(300)),
|
State(expiryMode = ExpiryMode.AfterRead(300)),
|
||||||
State(expiryMode = ExpiryMode.AfterSend(43200)),
|
State(expiryMode = ExpiryMode.AfterSend(43200)),
|
||||||
// new group non-admin
|
// new group non-admin
|
||||||
State(isGroup = true, isSelfAdmin = false),
|
State(isGroup = true, isSelfAdmin = false),
|
||||||
State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.Legacy(43200)),
|
|
||||||
State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.AfterSend(43200)),
|
State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.AfterSend(43200)),
|
||||||
// new group admin
|
// new group admin
|
||||||
State(isGroup = true),
|
State(isGroup = true),
|
||||||
State(isGroup = true, expiryMode = ExpiryMode.Legacy(43200)),
|
|
||||||
State(isGroup = true, expiryMode = ExpiryMode.AfterSend(43200)),
|
State(isGroup = true, expiryMode = ExpiryMode.AfterSend(43200)),
|
||||||
// new note-to-self
|
// new note-to-self
|
||||||
State(isNoteToSelf = true),
|
State(isNoteToSelf = true),
|
||||||
|
@ -104,7 +104,6 @@ import org.session.libsignal.utilities.guava.Optional
|
|||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.SessionDialogBuilder
|
|
||||||
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
||||||
import org.thoughtcrime.securesms.audio.AudioRecorder
|
import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
||||||
@ -117,6 +116,8 @@ import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companio
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
|
||||||
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
|
||||||
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_COPY
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_SAVE
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
||||||
@ -1935,7 +1936,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
} else {
|
} else {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.RECORD_AUDIO)
|
.request(Manifest.permission.RECORD_AUDIO)
|
||||||
.withRationaleDialog(getString(R.string.permissionsMicrophoneAccessRequired), R.drawable.ic_baseline_mic_48)
|
|
||||||
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired)
|
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired)
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
.format().toString())
|
.format().toString())
|
||||||
@ -2204,6 +2204,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
ON_REPLY -> reply(set)
|
ON_REPLY -> reply(set)
|
||||||
ON_RESEND -> resendMessage(set)
|
ON_RESEND -> resendMessage(set)
|
||||||
ON_DELETE -> deleteMessages(set)
|
ON_DELETE -> deleteMessages(set)
|
||||||
|
ON_COPY -> copyMessages(set)
|
||||||
|
ON_SAVE -> {
|
||||||
|
if(message is MmsMessageRecord) saveAttachmentsIfPossible(setOf(message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2238,7 +2242,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
return result == PackageManager.PERMISSION_GRANTED
|
return result == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveAttachment(messages: Set<MessageRecord>) {
|
override fun saveAttachmentsIfPossible(messages: Set<MessageRecord>) {
|
||||||
val message = messages.first() as MmsMessageRecord
|
val message = messages.first() as MmsMessageRecord
|
||||||
|
|
||||||
// Note: The save option is only added to the menu in ConversationReactionOverlay.getMenuActionItems
|
// Note: The save option is only added to the menu in ConversationReactionOverlay.getMenuActionItems
|
||||||
@ -2253,8 +2257,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// that we've warned the user just _once_ that any attachments they save can be accessed by other apps.
|
// that we've warned the user just _once_ that any attachments they save can be accessed by other apps.
|
||||||
val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(this)
|
val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(this)
|
||||||
if (haveWarned) {
|
if (haveWarned) {
|
||||||
// On Android versions below 30 we require the WRITE_EXTERNAL_STORAGE permission to save attachments.
|
// On Android versions below 29 we require the WRITE_EXTERNAL_STORAGE permission to save attachments.
|
||||||
if (Build.VERSION.SDK_INT < 30) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
// Save the attachment(s) then bail if we already have permission to do so
|
// Save the attachment(s) then bail if we already have permission to do so
|
||||||
if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
saveAttachments(message)
|
saveAttachments(message)
|
||||||
@ -2275,7 +2279,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P) // P is 28
|
.maxSdkVersion(Build.VERSION_CODES.P) // P is 28
|
||||||
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
|
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageDeniedLegacy)
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
.format().toString())
|
.format().toString())
|
||||||
.onAnyDenied {
|
.onAnyDenied {
|
||||||
@ -2285,7 +2289,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.permissionsRequired)
|
title(R.string.permissionsRequired)
|
||||||
|
|
||||||
val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
|
val txt = Phrase.from(applicationContext, R.string.permissionsStorageDeniedLegacy)
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
.format().toString()
|
.format().toString()
|
||||||
text(txt)
|
text(txt)
|
||||||
@ -2425,7 +2429,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
|
ConversationReactionOverlay.Action.REPLY -> reply(selectedItems)
|
||||||
ConversationReactionOverlay.Action.RESYNC -> resyncMessage(selectedItems)
|
ConversationReactionOverlay.Action.RESYNC -> resyncMessage(selectedItems)
|
||||||
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
|
ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems)
|
||||||
ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems)
|
ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachmentsIfPossible(selectedItems)
|
||||||
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)
|
ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems)
|
||||||
ConversationReactionOverlay.Action.VIEW_INFO -> showMessageDetail(selectedItems)
|
ConversationReactionOverlay.Action.VIEW_INFO -> showMessageDetail(selectedItems)
|
||||||
ConversationReactionOverlay.Action.SELECT -> selectMessages(selectedItems)
|
ConversationReactionOverlay.Action.SELECT -> selectMessages(selectedItems)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import android.util.SparseBooleanArray
|
import android.util.SparseBooleanArray
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
@ -30,6 +32,11 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.MissingMicrophonePermissionDialog
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||||
@ -121,7 +128,11 @@ class ConversationAdapter(
|
|||||||
val senderId = message.individualRecipient.address.serialize()
|
val senderId = message.individualRecipient.address.serialize()
|
||||||
val senderIdHash = senderId.hashCode()
|
val senderIdHash = senderId.hashCode()
|
||||||
updateQueue.trySend(senderId)
|
updateQueue.trySend(senderId)
|
||||||
if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(senderIdHash, false)) {
|
if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(
|
||||||
|
senderIdHash,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) {
|
||||||
getSenderInfo(senderId)?.let { contact ->
|
getSenderInfo(senderId)?.let { contact ->
|
||||||
contactCache[senderIdHash] = contact
|
contactCache[senderIdHash] = contact
|
||||||
}
|
}
|
||||||
@ -143,36 +154,27 @@ class ConversationAdapter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!message.isDeleted) {
|
if (!message.isDeleted) {
|
||||||
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
|
visibleMessageView.onPress = { event ->
|
||||||
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
onItemPress(
|
||||||
visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) }
|
message,
|
||||||
|
viewHolder.adapterPosition,
|
||||||
|
visibleMessageView,
|
||||||
|
event
|
||||||
|
)
|
||||||
|
}
|
||||||
|
visibleMessageView.onSwipeToReply =
|
||||||
|
{ onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||||
|
visibleMessageView.onLongPress =
|
||||||
|
{ onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) }
|
||||||
} else {
|
} else {
|
||||||
visibleMessageView.onPress = null
|
visibleMessageView.onPress = null
|
||||||
visibleMessageView.onSwipeToReply = null
|
visibleMessageView.onSwipeToReply = null
|
||||||
visibleMessageView.onLongPress = null
|
visibleMessageView.onLongPress = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ControlMessageViewHolder -> {
|
is ControlMessageViewHolder -> {
|
||||||
viewHolder.view.bind(message, messageBefore)
|
viewHolder.view.bind(message, messageBefore)
|
||||||
if (message.isCallLog && message.isFirstMissedCall) {
|
|
||||||
viewHolder.view.setOnClickListener {
|
|
||||||
context.showSessionDialog {
|
|
||||||
val titleTxt = context.getSubbedString(R.string.callsMissedCallFrom, NAME_KEY to message.individualRecipient.name!!)
|
|
||||||
title(titleTxt)
|
|
||||||
|
|
||||||
val bodyTxt = context.getSubbedCharSequence(R.string.callsYouMissedCallPermissions, NAME_KEY to message.individualRecipient.name!!)
|
|
||||||
text(bodyTxt)
|
|
||||||
|
|
||||||
button(R.string.sessionSettings) {
|
|
||||||
Intent(context, PrivacySettingsActivity::class.java)
|
|
||||||
.let(context::startActivity)
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
viewHolder.view.setOnClickListener(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,6 @@ import androidx.core.view.doOnLayout
|
|||||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -52,6 +49,9 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
|||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener
|
import org.thoughtcrime.securesms.util.AnimationCompleteListener
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ConversationReactionOverlay : FrameLayout {
|
class ConversationReactionOverlay : FrameLayout {
|
||||||
@ -213,7 +213,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
endY = backgroundView.height + menuPadding + reactionBarTopPadding
|
endY = backgroundView.height + menuPadding + reactionBarTopPadding
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.height
|
endY = overlayHeight - contextMenu.getMaxHeight() - 2*menuPadding - conversationItemSnapshot.height
|
||||||
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding
|
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding
|
||||||
}
|
}
|
||||||
endApparentTop = endY
|
endApparentTop = endY
|
||||||
@ -538,7 +538,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
||||||
}
|
}
|
||||||
// Copy Account ID
|
// Copy Account ID
|
||||||
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
|
if (!recipient.isCommunityRecipient && message.isIncoming) {
|
||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
|
||||||
}
|
}
|
||||||
// Delete message
|
// Delete message
|
||||||
@ -572,7 +572,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
items += ActionItem(R.attr.menu_save_icon,
|
items += ActionItem(R.attr.menu_save_icon,
|
||||||
R.string.save,
|
R.string.save,
|
||||||
{ handleActionItemClicked(Action.DOWNLOAD) },
|
{ handleActionItemClicked(Action.DOWNLOAD) },
|
||||||
R.string.AccessibilityId_save
|
R.string.AccessibilityId_saveAttachment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,11 @@ import androidx.compose.foundation.layout.FlowRow
|
|||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
@ -46,6 +48,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||||
@ -58,6 +61,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
|
|||||||
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
|
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.ui.Avatar
|
import org.thoughtcrime.securesms.ui.Avatar
|
||||||
import org.thoughtcrime.securesms.ui.CarouselNextButton
|
import org.thoughtcrime.securesms.ui.CarouselNextButton
|
||||||
import org.thoughtcrime.securesms.ui.CarouselPrevButton
|
import org.thoughtcrime.securesms.ui.CarouselPrevButton
|
||||||
@ -95,6 +99,8 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
const val ON_REPLY = 1
|
const val ON_REPLY = 1
|
||||||
const val ON_RESEND = 2
|
const val ON_RESEND = 2
|
||||||
const val ON_DELETE = 3
|
const val ON_DELETE = 3
|
||||||
|
const val ON_COPY = 4
|
||||||
|
const val ON_SAVE = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
@ -121,11 +127,18 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MessageDetailsScreen() {
|
private fun MessageDetailsScreen() {
|
||||||
val state by viewModel.stateFlow.collectAsState()
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
|
|
||||||
|
// can only save if the there is a media attachment which has finished downloading.
|
||||||
|
val canSave = state.mmsRecord?.containsMediaSlide() == true
|
||||||
|
&& state.mmsRecord?.isMediaPending == false
|
||||||
|
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
state = state,
|
state = state,
|
||||||
onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
|
onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
|
||||||
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
||||||
|
onSave = if(canSave) { { setResultAndFinish(ON_SAVE) } } else null,
|
||||||
onDelete = { setResultAndFinish(ON_DELETE) },
|
onDelete = { setResultAndFinish(ON_DELETE) },
|
||||||
|
onCopy = { setResultAndFinish(ON_COPY) },
|
||||||
onClickImage = { viewModel.onClickImage(it) },
|
onClickImage = { viewModel.onClickImage(it) },
|
||||||
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
|
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
|
||||||
)
|
)
|
||||||
@ -146,7 +159,9 @@ fun MessageDetails(
|
|||||||
state: MessageDetailsState,
|
state: MessageDetailsState,
|
||||||
onReply: (() -> Unit)? = null,
|
onReply: (() -> Unit)? = null,
|
||||||
onResend: (() -> Unit)? = null,
|
onResend: (() -> Unit)? = null,
|
||||||
|
onSave: (() -> Unit)? = null,
|
||||||
onDelete: () -> Unit = {},
|
onDelete: () -> Unit = {},
|
||||||
|
onCopy: () -> Unit = {},
|
||||||
onClickImage: (Int) -> Unit = {},
|
onClickImage: (Int) -> Unit = {},
|
||||||
onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit = { _ -> }
|
onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit = { _ -> }
|
||||||
) {
|
) {
|
||||||
@ -180,9 +195,11 @@ fun MessageDetails(
|
|||||||
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
|
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
|
||||||
CellMetadata(state)
|
CellMetadata(state)
|
||||||
CellButtons(
|
CellButtons(
|
||||||
onReply,
|
onReply = onReply,
|
||||||
onResend,
|
onResend = onResend,
|
||||||
onDelete,
|
onSave = onSave,
|
||||||
|
onDelete = onDelete,
|
||||||
|
onCopy = onCopy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,7 +221,15 @@ fun CellMetadata(
|
|||||||
senderInfo?.let {
|
senderInfo?.let {
|
||||||
TitledView(state.fromTitle) {
|
TitledView(state.fromTitle) {
|
||||||
Row {
|
Row {
|
||||||
sender?.let { Avatar(it) }
|
sender?.let {
|
||||||
|
Avatar(
|
||||||
|
recipient = it,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.size(46.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(LocalDimensions.current.smallSpacing))
|
||||||
|
}
|
||||||
TitledMonospaceText(it)
|
TitledMonospaceText(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -218,7 +243,9 @@ fun CellMetadata(
|
|||||||
fun CellButtons(
|
fun CellButtons(
|
||||||
onReply: (() -> Unit)? = null,
|
onReply: (() -> Unit)? = null,
|
||||||
onResend: (() -> Unit)? = null,
|
onResend: (() -> Unit)? = null,
|
||||||
onDelete: () -> Unit = {},
|
onSave: (() -> Unit)? = null,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onCopy: () -> Unit
|
||||||
) {
|
) {
|
||||||
Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) {
|
Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) {
|
||||||
Column {
|
Column {
|
||||||
@ -230,6 +257,23 @@ fun CellButtons(
|
|||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LargeItemButton(
|
||||||
|
R.string.copy,
|
||||||
|
R.drawable.ic_copy,
|
||||||
|
onClick = onCopy
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
onSave?.let {
|
||||||
|
LargeItemButton(
|
||||||
|
R.string.save,
|
||||||
|
R.drawable.ic_baseline_save_24,
|
||||||
|
onClick = it
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
|
||||||
onResend?.let {
|
onResend?.let {
|
||||||
LargeItemButton(
|
LargeItemButton(
|
||||||
R.string.resend,
|
R.string.resend,
|
||||||
@ -238,6 +282,7 @@ fun CellButtons(
|
|||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
|
|
||||||
LargeItemButton(
|
LargeItemButton(
|
||||||
R.string.delete,
|
R.string.delete,
|
||||||
R.drawable.ic_delete,
|
R.drawable.ic_delete,
|
||||||
@ -319,6 +364,21 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewMessageDetailsButtons(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
|
||||||
|
) {
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
CellButtons(
|
||||||
|
onReply = {},
|
||||||
|
onResend = {},
|
||||||
|
onSave = {},
|
||||||
|
onDelete = {},
|
||||||
|
onCopy = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -102,7 +102,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
|
R.id.menu_context_resync -> delegate?.resyncMessage(selectedItems)
|
||||||
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
|
R.id.menu_context_resend -> delegate?.resendMessage(selectedItems)
|
||||||
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
R.id.menu_message_details -> delegate?.showMessageDetail(selectedItems)
|
||||||
R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems)
|
R.id.menu_context_save_attachment -> delegate?.saveAttachmentsIfPossible(selectedItems)
|
||||||
R.id.menu_context_reply -> delegate?.reply(selectedItems)
|
R.id.menu_context_reply -> delegate?.reply(selectedItems)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -126,7 +126,7 @@ interface ConversationActionModeCallbackDelegate {
|
|||||||
fun resyncMessage(messages: Set<MessageRecord>)
|
fun resyncMessage(messages: Set<MessageRecord>)
|
||||||
fun resendMessage(messages: Set<MessageRecord>)
|
fun resendMessage(messages: Set<MessageRecord>)
|
||||||
fun showMessageDetail(messages: Set<MessageRecord>)
|
fun showMessageDetail(messages: Set<MessageRecord>)
|
||||||
fun saveAttachment(messages: Set<MessageRecord>)
|
fun saveAttachmentsIfPossible(messages: Set<MessageRecord>)
|
||||||
fun reply(messages: Set<MessageRecord>)
|
fun reply(messages: Set<MessageRecord>)
|
||||||
fun destroyActionMode()
|
fun destroyActionMode()
|
||||||
}
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.menus
|
package org.thoughtcrime.securesms.conversation.v2.menus
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
import android.os.AsyncTask
|
import android.os.AsyncTask
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
@ -22,11 +24,14 @@ import network.loki.messenger.R
|
|||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.messaging.sending_receiving.leave
|
import org.session.libsession.messaging.sending_receiving.leave
|
||||||
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
|
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
import org.thoughtcrime.securesms.MissingMicrophonePermissionDialog
|
||||||
import org.thoughtcrime.securesms.media.MediaOverviewActivity
|
import org.thoughtcrime.securesms.media.MediaOverviewActivity
|
||||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
||||||
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
|
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
|
||||||
@ -36,6 +41,7 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
|
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
|
||||||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
|
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||||
import org.thoughtcrime.securesms.showMuteDialog
|
import org.thoughtcrime.securesms.showMuteDialog
|
||||||
@ -162,6 +168,7 @@ object ConversationMenuHelper {
|
|||||||
|
|
||||||
private fun call(context: Context, thread: Recipient) {
|
private fun call(context: Context, thread: Recipient) {
|
||||||
|
|
||||||
|
// if the user has not enabled voice/video calls
|
||||||
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
|
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
|
||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.callsPermissionsRequired)
|
title(R.string.callsPermissionsRequired)
|
||||||
@ -173,6 +180,12 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// or if the user has not granted audio/microphone permissions
|
||||||
|
else if (!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO)) {
|
||||||
|
Log.d("Loki", "Attempted to make a call without audio permissions")
|
||||||
|
MissingMicrophonePermissionDialog.show(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
WebRtcCallService.createCall(context, thread)
|
WebRtcCallService.createCall(context, thread)
|
||||||
.let(context::startService)
|
.let(context::startService)
|
||||||
@ -273,13 +286,13 @@ object ConversationMenuHelper {
|
|||||||
val accountID = TextSecurePreferences.getLocalNumber(context)
|
val accountID = TextSecurePreferences.getLocalNumber(context)
|
||||||
val isCurrentUserAdmin = admins.any { it.toString() == accountID }
|
val isCurrentUserAdmin = admins.any { it.toString() == accountID }
|
||||||
val message = if (isCurrentUserAdmin) {
|
val message = if (isCurrentUserAdmin) {
|
||||||
Phrase.from(context, R.string.groupLeaveDescriptionAdmin)
|
Phrase.from(context, R.string.groupDeleteDescription)
|
||||||
.put(GROUP_NAME_KEY, group.title)
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
.format().toString()
|
.format()
|
||||||
} else {
|
} else {
|
||||||
Phrase.from(context, R.string.groupLeaveDescription)
|
Phrase.from(context, R.string.groupLeaveDescription)
|
||||||
.put(GROUP_NAME_KEY, group.title)
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
.format().toString()
|
.format()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLeaveFailed() {
|
fun onLeaveFailed() {
|
||||||
@ -292,7 +305,7 @@ object ConversationMenuHelper {
|
|||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.groupLeave)
|
title(R.string.groupLeave)
|
||||||
text(message)
|
text(message)
|
||||||
button(R.string.yes) {
|
dangerButton(R.string.leave) {
|
||||||
try {
|
try {
|
||||||
val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
|
val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
|
||||||
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
|
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
|
||||||
@ -303,7 +316,7 @@ object ConversationMenuHelper {
|
|||||||
onLeaveFailed()
|
onLeaveFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button(R.string.no)
|
button(R.string.cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
@ -10,17 +13,26 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewControlMessageBinding
|
import network.loki.messenger.databinding.ViewControlMessageBinding
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
|
import org.thoughtcrime.securesms.MissingMicrophonePermissionDialog
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
|
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ControlMessageView : LinearLayout {
|
class ControlMessageView : LinearLayout {
|
||||||
@ -29,6 +41,12 @@ class ControlMessageView : LinearLayout {
|
|||||||
|
|
||||||
private val binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
|
private val infoDrawable by lazy {
|
||||||
|
val d = ResourcesCompat.getDrawable(resources, R.drawable.ic_info_outline_white_24dp, context.theme)
|
||||||
|
d?.setTint(context.getColorFromAttr(R.attr.message_received_text_color))
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
@ -77,26 +95,81 @@ class ControlMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.isMessageRequestResponse -> {
|
message.isMessageRequestResponse -> {
|
||||||
binding.textView.text = context.getString(R.string.messageRequestsAccepted)
|
val msgRecipient = message.recipient.address.serialize()
|
||||||
binding.root.contentDescription = Phrase.from(context, R.string.messageRequestYouHaveAccepted)
|
val me = TextSecurePreferences.getLocalNumber(context)
|
||||||
.put(NAME_KEY, message.individualRecipient.name)
|
binding.textView.text = if(me == msgRecipient) { // you accepted the user's request
|
||||||
.format()
|
val threadRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(message.threadId)
|
||||||
|
context.getSubbedCharSequence(
|
||||||
|
R.string.messageRequestYouHaveAccepted,
|
||||||
|
NAME_KEY to (threadRecipient?.name ?: "")
|
||||||
|
)
|
||||||
|
} else { // they accepted your request
|
||||||
|
context.getString(R.string.messageRequestsAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.contentDescription = context.getString(R.string.AccessibilityId_message_request_config_message)
|
||||||
}
|
}
|
||||||
message.isCallLog -> {
|
message.isCallLog -> {
|
||||||
val drawable = when {
|
val drawable = when {
|
||||||
message.isIncomingCall -> R.drawable.ic_incoming_call
|
message.isIncomingCall -> R.drawable.ic_incoming_call
|
||||||
message.isOutgoingCall -> R.drawable.ic_outgoing_call
|
message.isOutgoingCall -> R.drawable.ic_outgoing_call
|
||||||
message.isFirstMissedCall -> R.drawable.ic_info_outline_light
|
|
||||||
else -> R.drawable.ic_missed_call
|
else -> R.drawable.ic_missed_call
|
||||||
}
|
}
|
||||||
binding.textView.isVisible = false
|
binding.textView.isVisible = false
|
||||||
binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(ResourcesCompat.getDrawable(resources, drawable, context.theme), null, null, null)
|
binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
ResourcesCompat.getDrawable(resources, drawable, context.theme),
|
||||||
|
null, null, null)
|
||||||
binding.callTextView.text = messageBody
|
binding.callTextView.text = messageBody
|
||||||
|
|
||||||
if (message.expireStarted > 0 && message.expiresIn > 0) {
|
if (message.expireStarted > 0 && message.expiresIn > 0) {
|
||||||
binding.expirationTimerView.isVisible = true
|
binding.expirationTimerView.isVisible = true
|
||||||
binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove clicks by default
|
||||||
|
setOnClickListener(null)
|
||||||
|
hideInfo()
|
||||||
|
|
||||||
|
// handle click behaviour depending on criteria
|
||||||
|
if (message.isMissedCall || message.isFirstMissedCall) {
|
||||||
|
when {
|
||||||
|
// if we're currently missing the audio/microphone permission,
|
||||||
|
// show a dedicated permission dialog
|
||||||
|
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) -> {
|
||||||
|
showInfo()
|
||||||
|
setOnClickListener {
|
||||||
|
MissingMicrophonePermissionDialog.show(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the call toggle is disabled in the privacy screen,
|
||||||
|
// show a dedicated privacy dialog
|
||||||
|
!TextSecurePreferences.isCallNotificationsEnabled(context) -> {
|
||||||
|
showInfo()
|
||||||
|
setOnClickListener {
|
||||||
|
context.showSessionDialog {
|
||||||
|
val titleTxt = context.getSubbedString(
|
||||||
|
R.string.callsMissedCallFrom,
|
||||||
|
NAME_KEY to message.individualRecipient.name!!
|
||||||
|
)
|
||||||
|
title(titleTxt)
|
||||||
|
|
||||||
|
val bodyTxt = context.getSubbedCharSequence(
|
||||||
|
R.string.callsYouMissedCallPermissions,
|
||||||
|
NAME_KEY to message.individualRecipient.name!!
|
||||||
|
)
|
||||||
|
text(bodyTxt)
|
||||||
|
|
||||||
|
button(R.string.sessionSettings) {
|
||||||
|
Intent(context, PrivacySettingsActivity::class.java)
|
||||||
|
.let(context::startActivity)
|
||||||
|
}
|
||||||
|
cancelButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +177,24 @@ class ControlMessageView : LinearLayout {
|
|||||||
binding.callView.isVisible = message.isCallLog
|
binding.callView.isVisible = message.isCallLog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showInfo(){
|
||||||
|
binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
binding.callTextView.compoundDrawablesRelative.first(),
|
||||||
|
null,
|
||||||
|
infoDrawable,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideInfo(){
|
||||||
|
binding.callTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
binding.callTextView.compoundDrawablesRelative.first(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,16 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
attachments.audioSlide != null -> {
|
attachments.audioSlide != null -> {
|
||||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||||
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.audio)
|
// A missing file name is the legacy way to determine if an audio attachment is
|
||||||
|
// a voice note vs. other arbitrary audio attachments.
|
||||||
|
val attachment = attachments.asAttachments().firstOrNull()
|
||||||
|
val isVoiceNote = attachment?.isVoiceNote == true ||
|
||||||
|
attachment != null && attachment.fileName.isNullOrEmpty()
|
||||||
|
binding.quoteViewBodyTextView.text = if (isVoiceNote) {
|
||||||
|
resources.getString(R.string.messageVoice)
|
||||||
|
} else {
|
||||||
|
resources.getString(R.string.audio)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
attachments.documentSlide != null -> {
|
attachments.documentSlide != null -> {
|
||||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||||
|
@ -61,6 +61,8 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager
|
|||||||
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.toDp
|
import org.thoughtcrime.securesms.util.toDp
|
||||||
@ -391,9 +393,9 @@ class VisibleMessageView : FrameLayout {
|
|||||||
context.getColor(R.color.accent_orange),
|
context.getColor(R.color.accent_orange),
|
||||||
R.string.messageStatusFailedToSync
|
R.string.messageStatusFailedToSync
|
||||||
)
|
)
|
||||||
message.isPending ->
|
message.isPending -> {
|
||||||
// Non-mms messages display 'Sending'..
|
// Non-mms messages (or quote messages, which happen to be mms for some reason) display 'Sending'..
|
||||||
if (!message.isMms) {
|
if (!message.isMms || (message as? MmsMessageRecord)?.quote != null) {
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
@ -404,9 +406,10 @@ class VisibleMessageView : FrameLayout {
|
|||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
R.string.messageStatusUploading
|
R.string.uploading
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
message.isSyncing || message.isResyncing ->
|
message.isSyncing || message.isResyncing ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
|
@ -245,51 +245,58 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
public static void selectDocument(Activity activity, int requestCode) {
|
public static void selectDocument(Activity activity, int requestCode) {
|
||||||
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
||||||
|
Context c = activity.getApplicationContext();
|
||||||
|
|
||||||
// The READ_EXTERNAL_STORAGE permission is deprecated (and will AUTO-FAIL if requested!) on
|
// The READ_EXTERNAL_STORAGE permission is deprecated (and will AUTO-FAIL if requested!) on
|
||||||
// Android 13 and above (API 33 - 'Tiramisu') we must ask for READ_MEDIA_VIDEO/IMAGES/AUDIO instead.
|
// Android 13 and above (API 33 - 'Tiramisu') we must ask for READ_MEDIA_VIDEO/IMAGES/AUDIO instead.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
||||||
.request(Manifest.permission.READ_MEDIA_IMAGES)
|
.request(Manifest.permission.READ_MEDIA_IMAGES)
|
||||||
.request(Manifest.permission.READ_MEDIA_AUDIO);
|
.request(Manifest.permission.READ_MEDIA_AUDIO)
|
||||||
|
.withRationaleDialog(
|
||||||
|
Phrase.from(c, R.string.permissionsStorageSend)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString()
|
||||||
|
)
|
||||||
|
.withPermanentDenialDialog(
|
||||||
|
Phrase.from(c, R.string.permissionMusicAudioDenied)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
.withPermanentDenialDialog(
|
||||||
|
Phrase.from(c, R.string.permissionsStorageDeniedLegacy)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Context c = activity.getApplicationContext();
|
builder.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
|
||||||
|
|
||||||
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString();
|
|
||||||
|
|
||||||
String storagePermissionDeniedTxt = Phrase.from(c, R.string.permissionsStorageSaveDenied)
|
|
||||||
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
|
||||||
.format().toString();
|
|
||||||
|
|
||||||
builder.withPermanentDenialDialog(storagePermissionDeniedTxt)
|
|
||||||
.withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
|
|
||||||
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
|
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||||
|
|
||||||
Context c = activity.getApplicationContext();
|
Context c = activity.getApplicationContext();
|
||||||
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend)
|
|
||||||
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
|
||||||
.format().toString();
|
|
||||||
String cameraPermissionDeniedTxt = Phrase.from(c, R.string.cameraGrantAccessDenied)
|
|
||||||
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
|
||||||
.format().toString();
|
|
||||||
|
|
||||||
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
||||||
.request(Manifest.permission.READ_MEDIA_IMAGES);
|
.request(Manifest.permission.READ_MEDIA_IMAGES)
|
||||||
|
.withPermanentDenialDialog(
|
||||||
|
Phrase.from(c, R.string.permissionsStorageDenied)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
.withPermanentDenialDialog(
|
||||||
|
Phrase.from(c, R.string.permissionsStorageDeniedLegacy)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
builder.withPermanentDenialDialog(cameraPermissionDeniedTxt)
|
builder.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||||
.withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
|
|
||||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,18 +320,13 @@ public class AttachmentManager {
|
|||||||
|
|
||||||
public void capturePhoto(Activity activity, int requestCode, Recipient recipient) {
|
public void capturePhoto(Activity activity, int requestCode, Recipient recipient) {
|
||||||
|
|
||||||
String cameraPermissionDeniedTxt = Phrase.from(context, R.string.cameraGrantAccessDenied)
|
String cameraPermissionDeniedTxt = Phrase.from(context, R.string.permissionsCameraDenied)
|
||||||
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
|
||||||
.format().toString();
|
|
||||||
|
|
||||||
String requireCameraPermissionTxt = Phrase.from(context, R.string.cameraGrantAccessDescription)
|
|
||||||
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
||||||
.format().toString();
|
.format().toString();
|
||||||
|
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.withPermanentDenialDialog(cameraPermissionDeniedTxt)
|
.withPermanentDenialDialog(cameraPermissionDeniedTxt)
|
||||||
.withRationaleDialog(requireCameraPermissionTxt, R.drawable.ic_baseline_photo_camera_24)
|
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
|
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
|
||||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||||
|
@ -234,7 +234,8 @@ public interface MmsSmsColumns {
|
|||||||
|
|
||||||
public static boolean isCallLog(long type) {
|
public static boolean isCallLog(long type) {
|
||||||
long baseType = type & BASE_TYPE_MASK;
|
long baseType = type & BASE_TYPE_MASK;
|
||||||
return baseType == INCOMING_CALL_TYPE || baseType == OUTGOING_CALL_TYPE || baseType == MISSED_CALL_TYPE || baseType == FIRST_MISSED_CALL_TYPE;
|
return baseType == INCOMING_CALL_TYPE || baseType == OUTGOING_CALL_TYPE ||
|
||||||
|
baseType == MISSED_CALL_TYPE || baseType == FIRST_MISSED_CALL_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isExpirationTimerUpdate(long type) {
|
public static boolean isExpirationTimerUpdate(long type) {
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import network.loki.messenger.R
|
||||||
import java.security.MessageDigest
|
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
|
||||||
@ -1448,7 +1449,10 @@ open class Storage(
|
|||||||
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(sentTimestamp, senderPublicKey, expiryMode)
|
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(sentTimestamp, senderPublicKey, expiryMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
|
/**
|
||||||
|
* This will create a control message used to indicate that a contact has accepted our message request
|
||||||
|
*/
|
||||||
|
override fun insertMessageRequestResponseFromContact(response: MessageRequestResponse) {
|
||||||
val userPublicKey = getUserPublicKey()
|
val userPublicKey = getUserPublicKey()
|
||||||
val senderPublicKey = response.sender!!
|
val senderPublicKey = response.sender!!
|
||||||
val recipientPublicKey = response.recipient!!
|
val recipientPublicKey = response.recipient!!
|
||||||
@ -1542,6 +1546,34 @@ open class Storage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will create a control message used to indicate that you have accepted a message request
|
||||||
|
*/
|
||||||
|
override fun insertMessageRequestResponseFromYou(threadId: Long){
|
||||||
|
val userPublicKey = getUserPublicKey() ?: return
|
||||||
|
|
||||||
|
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
val message = IncomingMediaMessage(
|
||||||
|
fromSerialized(userPublicKey),
|
||||||
|
SnodeAPI.nowWithOffset,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent()
|
||||||
|
)
|
||||||
|
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getRecipientApproved(address: Address): Boolean {
|
override fun getRecipientApproved(address: Address): Boolean {
|
||||||
return DatabaseComponent.get(context).recipientDatabase().getApproved(address)
|
return DatabaseComponent.get(context).recipientDatabase().getApproved(address)
|
||||||
}
|
}
|
||||||
|
@ -17,15 +17,11 @@
|
|||||||
package org.thoughtcrime.securesms.database.model;
|
package org.thoughtcrime.securesms.database.model;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.SpannableString;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,7 +64,7 @@ public abstract class DisplayRecord {
|
|||||||
public @NonNull String getBody() {
|
public @NonNull String getBody() {
|
||||||
return body == null ? "" : body;
|
return body == null ? "" : body;
|
||||||
}
|
}
|
||||||
public abstract SpannableString getDisplayBody(@NonNull Context context);
|
public abstract CharSequence getDisplayBody(@NonNull Context context);
|
||||||
public Recipient getRecipient() { return recipient; }
|
public Recipient getRecipient() { return recipient; }
|
||||||
public long getDateSent() { return dateSent; }
|
public long getDateSent() { return dateSent; }
|
||||||
public long getDateReceived() { return dateReceived; }
|
public long getDateReceived() { return dateReceived; }
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package org.thoughtcrime.securesms.database.model;
|
package org.thoughtcrime.securesms.database.model;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.SpannableString;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -27,14 +26,11 @@ import org.session.libsession.utilities.Contact;
|
|||||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||||
import org.session.libsession.utilities.NetworkFailure;
|
import org.session.libsession.utilities.NetworkFailure;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the message record model for MMS messages that contain
|
* Represents the message record model for MMS messages that contain
|
||||||
* media (ie: they've been downloaded).
|
* media (ie: they've been downloaded).
|
||||||
@ -76,7 +72,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public CharSequence getDisplayBody(@NonNull Context context) {
|
||||||
return super.getDisplayBody(context);
|
return super.getDisplayBody(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public CharSequence getDisplayBody(@NonNull Context context) {
|
||||||
if (isGroupUpdateMessage()) {
|
if (isGroupUpdateMessage()) {
|
||||||
UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody());
|
UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody());
|
||||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
||||||
|
@ -18,14 +18,13 @@
|
|||||||
package org.thoughtcrime.securesms.database.model;
|
package org.thoughtcrime.securesms.database.model;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.SpannableString;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The message record model which represents standard SMS messages.
|
* The message record model which represents standard SMS messages.
|
||||||
@ -56,7 +55,7 @@ public class SmsMessageRecord extends MessageRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public CharSequence getDisplayBody(@NonNull Context context) {
|
||||||
return super.getDisplayBody(context);
|
return super.getDisplayBody(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,9 @@
|
|||||||
package org.thoughtcrime.securesms.database.model;
|
package org.thoughtcrime.securesms.database.model;
|
||||||
|
|
||||||
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.AUTHOR_KEY;
|
||||||
import static org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY;
|
import static org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY;
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.MESSAGE_SNIPPET_KEY;
|
||||||
import static org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY;
|
import static org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY;
|
||||||
import static org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY;
|
import static org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY;
|
||||||
|
|
||||||
@ -32,10 +34,14 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.squareup.phrase.Phrase;
|
import com.squareup.phrase.Phrase;
|
||||||
import org.session.libsession.utilities.ExpirationUtil;
|
import org.session.libsession.utilities.ExpirationUtil;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
|
import org.thoughtcrime.securesms.ui.UtilKt;
|
||||||
|
|
||||||
|
import kotlin.Pair;
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,67 +119,68 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public CharSequence getDisplayBody(@NonNull Context context) {
|
||||||
if (isGroupUpdateMessage()) {
|
if (isGroupUpdateMessage()) {
|
||||||
return emphasisAdded(context.getString(R.string.groupUpdated));
|
return context.getString(R.string.groupUpdated);
|
||||||
} else if (isOpenGroupInvitation()) {
|
} else if (isOpenGroupInvitation()) {
|
||||||
return emphasisAdded(context.getString(R.string.communityInvitation));
|
return context.getString(R.string.communityInvitation);
|
||||||
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
||||||
String txt = Phrase.from(context, R.string.messageErrorOld)
|
return Phrase.from(context, R.string.messageErrorOld)
|
||||||
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
|
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
|
||||||
String draftText = context.getString(R.string.draft);
|
String draftText = context.getString(R.string.draft);
|
||||||
return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
|
return draftText + " " + getBody();
|
||||||
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
|
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
|
||||||
String txt = Phrase.from(context, R.string.callsYouCalled)
|
return Phrase.from(context, R.string.callsYouCalled)
|
||||||
.put(NAME_KEY, getName())
|
.put(NAME_KEY, getName())
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
} else if (SmsDatabase.Types.isIncomingCall(type)) {
|
} else if (SmsDatabase.Types.isIncomingCall(type)) {
|
||||||
String txt = Phrase.from(context, R.string.callsCalledYou)
|
return Phrase.from(context, R.string.callsCalledYou)
|
||||||
.put(NAME_KEY, getName())
|
.put(NAME_KEY, getName())
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
} else if (SmsDatabase.Types.isMissedCall(type)) {
|
} else if (SmsDatabase.Types.isMissedCall(type)) {
|
||||||
String txt = Phrase.from(context, R.string.callsMissedCallFrom)
|
return Phrase.from(context, R.string.callsMissedCallFrom)
|
||||||
.put(NAME_KEY, getName())
|
.put(NAME_KEY, getName())
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
|
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
|
||||||
int seconds = (int) (getExpiresIn() / 1000);
|
int seconds = (int) (getExpiresIn() / 1000);
|
||||||
if (seconds <= 0) {
|
if (seconds <= 0) {
|
||||||
String txt = Phrase.from(context, R.string.disappearingMessagesTurnedOff)
|
return Phrase.from(context, R.string.disappearingMessagesTurnedOff)
|
||||||
.put(NAME_KEY, getName())
|
.put(NAME_KEY, getName())
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implied that disappearing messages is enabled..
|
// Implied that disappearing messages is enabled..
|
||||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||||
String disappearAfterWhat = getDisappearingMsgExpiryTypeString(context); // Disappear after send or read?
|
String disappearAfterWhat = getDisappearingMsgExpiryTypeString(context); // Disappear after send or read?
|
||||||
String txt = Phrase.from(context, R.string.disappearingMessagesSet)
|
return Phrase.from(context, R.string.disappearingMessagesSet)
|
||||||
.put(NAME_KEY, getName())
|
.put(NAME_KEY, getName())
|
||||||
.put(TIME_KEY, time)
|
.put(TIME_KEY, time)
|
||||||
.put(DISAPPEARING_MESSAGES_TYPE_KEY, disappearAfterWhat)
|
.put(DISAPPEARING_MESSAGES_TYPE_KEY, disappearAfterWhat)
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
|
|
||||||
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
|
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
|
||||||
String txt = Phrase.from(context, R.string.attachmentsMediaSaved)
|
return Phrase.from(context, R.string.attachmentsMediaSaved)
|
||||||
.put(NAME_KEY, getName())
|
.put(NAME_KEY, getName())
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
|
|
||||||
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
|
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
|
||||||
String txt = Phrase.from(context, R.string.screenshotTaken)
|
return Phrase.from(context, R.string.screenshotTaken)
|
||||||
.put(NAME_KEY, getName())
|
.put(NAME_KEY, getName())
|
||||||
.format().toString();
|
.format().toString();
|
||||||
return emphasisAdded(txt);
|
|
||||||
|
|
||||||
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
|
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
|
||||||
return emphasisAdded(context.getString(R.string.messageRequestsAccepted));
|
if (lastMessage.getRecipient().getAddress().serialize().equals(
|
||||||
|
TextSecurePreferences.getLocalNumber(context))) {
|
||||||
|
return UtilKt.getSubbedCharSequence(
|
||||||
|
context,
|
||||||
|
R.string.messageRequestYouHaveAccepted,
|
||||||
|
new Pair<>(NAME_KEY, getName())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.getString(R.string.messageRequestsAccepted);
|
||||||
} else if (getCount() == 0) {
|
} else if (getCount() == 0) {
|
||||||
return new SpannableString(context.getString(R.string.messageEmpty));
|
return new SpannableString(context.getString(R.string.messageEmpty));
|
||||||
} else {
|
} else {
|
||||||
@ -186,20 +193,37 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
return new SpannableString("");
|
return new SpannableString("");
|
||||||
// Old behaviour was: return new SpannableString(emphasisAdded(context.getString(R.string.mediaMessage)));
|
// Old behaviour was: return new SpannableString(emphasisAdded(context.getString(R.string.mediaMessage)));
|
||||||
} else {
|
} else {
|
||||||
return new SpannableString(getBody());
|
return getNonControlMessageDisplayBody(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SpannableString emphasisAdded(String sequence) {
|
/**
|
||||||
return emphasisAdded(sequence, 0, sequence.length());
|
* Logic to get the body for non control messages
|
||||||
|
*/
|
||||||
|
public CharSequence getNonControlMessageDisplayBody(@NonNull Context context) {
|
||||||
|
Recipient recipient = getRecipient();
|
||||||
|
// The logic will differ depending on the type.
|
||||||
|
// 1-1, note to self and control messages (we shouldn't have any in here, but leaving the
|
||||||
|
// logic to be safe) do not need author details
|
||||||
|
if (recipient.isLocalNumber() || recipient.is1on1() ||
|
||||||
|
(lastMessage != null && lastMessage.isControlMessage())
|
||||||
|
) {
|
||||||
|
return getBody();
|
||||||
|
} else { // for groups (new, legacy, communities) show either 'You' or the contact's name
|
||||||
|
String prefix = "";
|
||||||
|
if (lastMessage != null && lastMessage.isOutgoing()) {
|
||||||
|
prefix = context.getString(R.string.you);
|
||||||
|
}
|
||||||
|
else if(lastMessage != null){
|
||||||
|
prefix = lastMessage.getIndividualRecipient().toShortString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SpannableString emphasisAdded(String sequence, int start, int end) {
|
return Phrase.from(context.getString(R.string.messageSnippetGroup))
|
||||||
SpannableString spannable = new SpannableString(sequence);
|
.put(AUTHOR_KEY, prefix)
|
||||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
.put(MESSAGE_SNIPPET_KEY, getBody())
|
||||||
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
.format().toString();
|
||||||
return spannable;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getCount() { return count; }
|
public long getCount() { return count; }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.debugmenu
|
package org.thoughtcrime.securesms.debugmenu
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -19,6 +20,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import network.loki.messenger.BuildConfig
|
import network.loki.messenger.BuildConfig
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
@ -102,13 +105,22 @@ fun DebugMenu(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
// Info pane
|
// Info pane
|
||||||
DebugCell("App Info") {
|
val clipboardManager = LocalClipboardManager.current
|
||||||
Text(
|
val appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - ${
|
||||||
text = "Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - ${
|
|
||||||
BuildConfig.GIT_HASH.take(
|
BuildConfig.GIT_HASH.take(
|
||||||
6
|
6
|
||||||
)
|
)
|
||||||
})",
|
})"
|
||||||
|
|
||||||
|
DebugCell(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
// clicking the cell copies the version number to the clipboard
|
||||||
|
clipboardManager.setText(AnnotatedString(appVersion))
|
||||||
|
},
|
||||||
|
title = "App Info"
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Version: $appVersion",
|
||||||
style = LocalType.current.base
|
style = LocalType.current.base
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import org.session.libsession.utilities.recipients.Recipient
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@ -37,6 +38,8 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
|
|
||||||
lateinit var delegate: StartConversationDelegate
|
lateinit var delegate: StartConversationDelegate
|
||||||
|
|
||||||
|
var lastUrl: String? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
@ -66,34 +69,62 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun joinCommunityIfPossible(url: String) {
|
fun joinCommunityIfPossible(url: String) {
|
||||||
|
// Currently this won't try again on a failed URL but once we rework the whole
|
||||||
|
// fragment into Compose with a ViewModel this won't be an issue anymore as the error
|
||||||
|
// and state will come from Flows.
|
||||||
|
if(lastUrl == url) return
|
||||||
|
lastUrl = url
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val openGroup = try {
|
val openGroup = try {
|
||||||
OpenGroupUrlParser.parseUrl(url)
|
OpenGroupUrlParser.parseUrl(url)
|
||||||
} catch (e: OpenGroupUrlParser.Error) {
|
} catch (e: OpenGroupUrlParser.Error) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is OpenGroupUrlParser.Error.MalformedURL, OpenGroupUrlParser.Error.NoRoom -> {
|
is OpenGroupUrlParser.Error.MalformedURL, OpenGroupUrlParser.Error.NoRoom -> {
|
||||||
return Toast.makeText(activity, context?.resources?.getString(R.string.communityJoinError), Toast.LENGTH_SHORT).show()
|
return@launch Toast.makeText(
|
||||||
|
activity,
|
||||||
|
context?.resources?.getString(R.string.communityJoinError),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
is OpenGroupUrlParser.Error.InvalidPublicKey, OpenGroupUrlParser.Error.NoPublicKey -> {
|
is OpenGroupUrlParser.Error.InvalidPublicKey, OpenGroupUrlParser.Error.NoPublicKey -> {
|
||||||
return Toast.makeText(activity, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT).show()
|
return@launch Toast.makeText(
|
||||||
|
activity,
|
||||||
|
R.string.communityEnterUrlErrorInvalidDescription,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader()
|
showLoader()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val sanitizedServer = openGroup.server.removeSuffix("/")
|
val sanitizedServer = openGroup.server.removeSuffix("/")
|
||||||
val openGroupID = "$sanitizedServer.${openGroup.room}"
|
val openGroupID = "$sanitizedServer.${openGroup.room}"
|
||||||
OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, requireContext())
|
OpenGroupManager.add(
|
||||||
|
sanitizedServer,
|
||||||
|
openGroup.room,
|
||||||
|
openGroup.serverPublicKey,
|
||||||
|
requireContext()
|
||||||
|
)
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
storage.onOpenGroupAdded(sanitizedServer, openGroup.room)
|
storage.onOpenGroupAdded(sanitizedServer, openGroup.room)
|
||||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, requireContext())
|
val threadID =
|
||||||
|
GroupManager.getOpenGroupThreadID(openGroupID, requireContext())
|
||||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||||
|
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext())
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
|
||||||
|
requireContext()
|
||||||
|
)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(groupID), false)
|
val recipient = Recipient.from(
|
||||||
|
requireContext(),
|
||||||
|
Address.fromSerialized(groupID),
|
||||||
|
false
|
||||||
|
)
|
||||||
openConversationActivity(requireContext(), threadID, recipient)
|
openConversationActivity(requireContext(), threadID, recipient)
|
||||||
delegate.onDialogClosePressed()
|
delegate.onDialogClosePressed()
|
||||||
}
|
}
|
||||||
@ -101,10 +132,11 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
Log.e("Loki", "Couldn't join community.", e)
|
Log.e("Loki", "Couldn't join community.", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
hideLoader()
|
hideLoader()
|
||||||
val txt = Phrase.from(context, R.string.groupErrorJoin).put(GROUP_NAME_KEY, url).format().toString()
|
val txt = context?.getSubbedString(R.string.groupErrorJoin,
|
||||||
|
GROUP_NAME_KEY to url)
|
||||||
Toast.makeText(activity, txt, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, txt, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
return@launch
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
@ -82,7 +83,33 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
|
|||||||
binding.muteNotificationsTextView.setOnClickListener(this)
|
binding.muteNotificationsTextView.setOnClickListener(this)
|
||||||
binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted
|
||||||
binding.notificationsTextView.setOnClickListener(this)
|
binding.notificationsTextView.setOnClickListener(this)
|
||||||
binding.deleteTextView.setOnClickListener(this)
|
|
||||||
|
// delete
|
||||||
|
binding.deleteTextView.apply {
|
||||||
|
setOnClickListener(this@ConversationOptionsBottomSheet)
|
||||||
|
|
||||||
|
// the text and content description will change depending on the type
|
||||||
|
when{
|
||||||
|
// groups and communities
|
||||||
|
recipient.isGroupRecipient -> {
|
||||||
|
text = context.getString(R.string.leave)
|
||||||
|
contentDescription = context.getString(R.string.AccessibilityId_leave)
|
||||||
|
}
|
||||||
|
|
||||||
|
// note to self
|
||||||
|
recipient.isLocalNumber -> {
|
||||||
|
text = context.getString(R.string.clear)
|
||||||
|
contentDescription = context.getString(R.string.AccessibilityId_clear)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1on1
|
||||||
|
else -> {
|
||||||
|
text = context.getString(R.string.delete)
|
||||||
|
contentDescription = context.getString(R.string.AccessibilityId_delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 || configFactory.convoVolatile?.getConversationUnread(thread) == true
|
binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 || configFactory.convoVolatile?.getConversationUnread(thread) == true
|
||||||
binding.markAllAsReadTextView.setOnClickListener(this)
|
binding.markAllAsReadTextView.setOnClickListener(this)
|
||||||
binding.pinTextView.isVisible = !thread.isPinned
|
binding.pinTextView.isVisible = !thread.isPinned
|
||||||
|
@ -103,7 +103,7 @@ class ConversationView : LinearLayout {
|
|||||||
}
|
}
|
||||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||||
binding.snippetTextView.text = highlightMentions(
|
binding.snippetTextView.text = highlightMentions(
|
||||||
text = thread.getSnippet(),
|
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
|
||||||
@ -139,16 +139,5 @@ class ConversationView : LinearLayout {
|
|||||||
recipient.isLocalNumber -> context.getString(R.string.noteToSelf)
|
recipient.isLocalNumber -> context.getString(R.string.noteToSelf)
|
||||||
else -> recipient.toShortString() // Internally uses the Contact API
|
else -> recipient.toShortString() // Internally uses the Contact API
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ThreadRecord.getSnippet(): CharSequence = listOfNotNull(
|
|
||||||
getSnippetPrefix(),
|
|
||||||
getDisplayBody(context)
|
|
||||||
).joinToString(": ")
|
|
||||||
|
|
||||||
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
|
||||||
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
|
||||||
lastMessage?.isOutgoing == true -> resources.getString(R.string.you)
|
|
||||||
else -> lastMessage?.individualRecipient?.toShortString()
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.NonTranslatableStringConstants.WAVING_HAND_EMOJI
|
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
|
||||||
import org.thoughtcrime.securesms.ui.Divider
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
@ -53,7 +52,7 @@ internal fun EmptyView(newAccount: Boolean) {
|
|||||||
val c = LocalContext.current
|
val c = LocalContext.current
|
||||||
Phrase.from(txt)
|
Phrase.from(txt)
|
||||||
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
.put(EMOJI_KEY, WAVING_HAND_EMOJI)
|
.put(EMOJI_KEY, "\uD83D\uDC4B") // this hardcoded emoji might be moved to NonTranslatableConstants eventually
|
||||||
.format().toString()
|
.format().toString()
|
||||||
},
|
},
|
||||||
style = LocalType.current.base,
|
style = LocalType.current.base,
|
||||||
|
@ -388,6 +388,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
EventBus.getDefault().unregister(this)
|
EventBus.getDefault().unregister(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
@ -547,6 +552,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
} else {
|
} else {
|
||||||
showMuteDialog(this) { until ->
|
showMuteDialog(this) { until ->
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
Log.d("", "**** until: $until")
|
||||||
recipientDatabase.setMuted(thread.recipient, until)
|
recipientDatabase.setMuted(thread.recipient, until)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
@ -583,13 +589,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
val recipient = thread.recipient
|
val recipient = thread.recipient
|
||||||
val title: String
|
val title: String
|
||||||
val message: CharSequence
|
val message: CharSequence
|
||||||
|
var positiveButtonId: Int = R.string.yes
|
||||||
|
var negativeButtonId: Int = R.string.no
|
||||||
|
|
||||||
if (recipient.isGroupRecipient) {
|
if (recipient.isGroupRecipient) {
|
||||||
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
||||||
|
|
||||||
// If you are an admin of this group you can delete it
|
// If you are an admin of this group you can delete it
|
||||||
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
||||||
title = getString(R.string.groupDelete)
|
title = getString(R.string.groupLeave)
|
||||||
message = Phrase.from(this.applicationContext, R.string.groupDeleteDescription)
|
message = Phrase.from(this.applicationContext, R.string.groupDeleteDescription)
|
||||||
.put(GROUP_NAME_KEY, group.title)
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
.format()
|
.format()
|
||||||
@ -600,6 +608,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
.put(GROUP_NAME_KEY, group.title)
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
.format()
|
.format()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
positiveButtonId = R.string.leave
|
||||||
|
negativeButtonId = R.string.cancel
|
||||||
} else {
|
} else {
|
||||||
// If this is a 1-on-1 conversation
|
// If this is a 1-on-1 conversation
|
||||||
if (recipient.name != null) {
|
if (recipient.name != null) {
|
||||||
@ -610,15 +621,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If not group-related and we don't have a recipient name then this must be our Note to Self conversation
|
// If not group-related and we don't have a recipient name then this must be our Note to Self conversation
|
||||||
title = getString(R.string.noteToSelf)
|
title = getString(R.string.clearMessages)
|
||||||
message = getString(R.string.clearMessagesNoteToSelfDescription)
|
message = getString(R.string.clearMessagesNoteToSelfDescription)
|
||||||
|
positiveButtonId = R.string.clear
|
||||||
|
negativeButtonId = R.string.cancel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(title)
|
title(title)
|
||||||
text(message)
|
text(message)
|
||||||
button(R.string.yes) {
|
dangerButton(positiveButtonId) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val context = this@HomeActivity
|
val context = this@HomeActivity
|
||||||
// Cancel any outstanding jobs
|
// Cancel any outstanding jobs
|
||||||
@ -650,7 +663,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button(R.string.no)
|
button(negativeButtonId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))
|
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))
|
||||||
getPathRow(snode, LineView.Location.Middle, index.toLong() * 1000 + 2000, dotAnimationRepeatInterval, isGuardSnode)
|
getPathRow(snode, LineView.Location.Middle, index.toLong() * 1000 + 2000, dotAnimationRepeatInterval, isGuardSnode)
|
||||||
}
|
}
|
||||||
val youRow = getPathRow(resources.getString(R.string.onionRoutingPath), null, LineView.Location.Top, 1000, dotAnimationRepeatInterval)
|
val youRow = getPathRow(resources.getString(R.string.you), null, LineView.Location.Top, 1000, dotAnimationRepeatInterval)
|
||||||
val destinationRow = getPathRow(resources.getString(R.string.onionRoutingPathDestination), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
val destinationRow = getPathRow(resources.getString(R.string.onionRoutingPathDestination), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
||||||
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
||||||
for (row in rows) {
|
for (row in rows) {
|
||||||
|
@ -34,7 +34,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@ -69,7 +68,7 @@ fun MediaOverviewScreen(
|
|||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
R.string.cameraGrantAccessDenied,
|
R.string.permissionsCameraDenied,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
@ -232,7 +231,7 @@ private fun SaveAttachmentWarningDialog(
|
|||||||
title = context.getString(R.string.warning),
|
title = context.getString(R.string.warning),
|
||||||
text = context.resources.getString(R.string.attachmentsWarning),
|
text = context.resources.getString(R.string.attachmentsWarning),
|
||||||
buttons = listOf(
|
buttons = listOf(
|
||||||
DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_save), color = LocalColors.current.danger, onClick = onAccepted),
|
DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_saveAttachment), color = LocalColors.current.danger, onClick = onAccepted),
|
||||||
DialogButtonModel(GetString(android.R.string.cancel), GetString(R.string.AccessibilityId_cancel), dismissOnClick = true)
|
DialogButtonModel(GetString(android.R.string.cancel), GetString(R.string.AccessibilityId_cancel), dismissOnClick = true)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -290,5 +289,5 @@ private fun ActionProgressDialog(
|
|||||||
private val MediaOverviewTab.titleResId: Int
|
private val MediaOverviewTab.titleResId: Int
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
MediaOverviewTab.Media -> R.string.media
|
MediaOverviewTab.Media -> R.string.media
|
||||||
MediaOverviewTab.Documents -> R.string.document
|
MediaOverviewTab.Documents -> R.string.files
|
||||||
}
|
}
|
@ -362,7 +362,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
private void navigateToCamera() {
|
private void navigateToCamera() {
|
||||||
|
|
||||||
Context c = getApplicationContext();
|
Context c = getApplicationContext();
|
||||||
String permanentDenialTxt = Phrase.from(c, R.string.cameraGrantAccessDenied)
|
String permanentDenialTxt = Phrase.from(c, R.string.permissionsCameraDenied)
|
||||||
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
.format().toString();
|
.format().toString();
|
||||||
String requireCameraPermissionsTxt = Phrase.from(c, R.string.cameraGrantAccessDescription)
|
String requireCameraPermissionsTxt = Phrase.from(c, R.string.cameraGrantAccessDescription)
|
||||||
@ -371,7 +371,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
|
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.withRationaleDialog(requireCameraPermissionsTxt, R.drawable.ic_baseline_photo_camera_48)
|
|
||||||
.withPermanentDenialDialog(permanentDenialTxt)
|
.withPermanentDenialDialog(permanentDenialTxt)
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
Camera1Fragment fragment = getOrCreateCameraFragment();
|
Camera1Fragment fragment = getOrCreateCameraFragment();
|
||||||
|
@ -49,14 +49,11 @@ abstract class Slide(@JvmField protected val context: Context, protected val att
|
|||||||
// A missing file name is the legacy way to determine if an audio attachment is
|
// A missing file name is the legacy way to determine if an audio attachment is
|
||||||
// a voice note vs. other arbitrary audio attachments.
|
// a voice note vs. other arbitrary audio attachments.
|
||||||
if (attachment.isVoiceNote || attachment.fileName.isNullOrEmpty()) {
|
if (attachment.isVoiceNote || attachment.fileName.isNullOrEmpty()) {
|
||||||
val baseString = context.getString(R.string.messageVoice)
|
val voiceTxt = Phrase.from(context, R.string.messageVoiceSnippet)
|
||||||
val languageIsLTR = Util.usingLeftToRightLanguage(context)
|
.put(EMOJI_KEY, "🎙")
|
||||||
val attachmentString = if (languageIsLTR) {
|
.format().toString()
|
||||||
"🎙 $baseString"
|
|
||||||
} else {
|
return Optional.fromNullable(voiceTxt)
|
||||||
"$baseString 🎙"
|
|
||||||
}
|
|
||||||
return Optional.fromNullable(attachmentString)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val txt = Phrase.from(context, R.string.attachmentsNotification)
|
val txt = Phrase.from(context, R.string.attachmentsNotification)
|
||||||
@ -66,19 +63,19 @@ abstract class Slide(@JvmField protected val context: Context, protected val att
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun emojiForMimeType(): String {
|
private fun emojiForMimeType(): String {
|
||||||
return if (MediaUtil.isGif(attachment)) {
|
return when{
|
||||||
"🎡"
|
MediaUtil.isGif(attachment) -> "🎡"
|
||||||
} else if (MediaUtil.isImage(attachment)) {
|
|
||||||
"📷"
|
MediaUtil.isImage(attachment) -> "📷"
|
||||||
} else if (MediaUtil.isVideo(attachment)) {
|
|
||||||
"🎥"
|
MediaUtil.isVideo(attachment) -> "🎥"
|
||||||
} else if (MediaUtil.isAudio(attachment)) {
|
|
||||||
"🎧"
|
MediaUtil.isAudio(attachment) -> "🎧"
|
||||||
} else if (MediaUtil.isFile(attachment)) {
|
|
||||||
"📎"
|
MediaUtil.isFile(attachment) -> "📎"
|
||||||
} else {
|
|
||||||
// We don't provide emojis for other mime-types such as VCARD
|
// We don't provide emojis for other mime-types such as VCARD
|
||||||
""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,11 +17,10 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors
|
|||||||
@Composable
|
@Composable
|
||||||
fun OnboardingBackPressAlertDialog(
|
fun OnboardingBackPressAlertDialog(
|
||||||
dismissDialog: () -> Unit,
|
dismissDialog: () -> Unit,
|
||||||
@StringRes textId: Int = R.string.onboardingBackAccountCreation,
|
@StringRes textId: Int,
|
||||||
quit: () -> Unit
|
quit: () -> Unit
|
||||||
) {
|
) {
|
||||||
val c = LocalContext.current
|
val c = LocalContext.current
|
||||||
val quitButtonText = c.getSubbedString(R.string.quit, APP_NAME_KEY to APP_NAME)
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = dismissDialog,
|
onDismissRequest = dismissDialog,
|
||||||
@ -31,7 +30,7 @@ fun OnboardingBackPressAlertDialog(
|
|||||||
},
|
},
|
||||||
buttons = listOf(
|
buttons = listOf(
|
||||||
DialogButtonModel(
|
DialogButtonModel(
|
||||||
text = GetString(quitButtonText),
|
text = GetString(stringResource(id = R.string.quitButton)),
|
||||||
color = LocalColors.current.danger,
|
color = LocalColors.current.danger,
|
||||||
onClick = quit
|
onClick = quit
|
||||||
),
|
),
|
||||||
|
@ -37,8 +37,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.NonTranslatableStringConstants.BACKHAND_INDEX_POINTING_DOWN_EMOJI
|
|
||||||
import org.session.libsession.utilities.NonTranslatableStringConstants.WAVING_HAND_EMOJI
|
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
|
||||||
import org.thoughtcrime.securesms.ui.AlertDialog
|
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||||
@ -139,7 +137,7 @@ internal fun LandingScreen(
|
|||||||
R.string.onboardingBubbleWelcomeToSession -> {
|
R.string.onboardingBubbleWelcomeToSession -> {
|
||||||
Phrase.from(stringResource(item.stringId))
|
Phrase.from(stringResource(item.stringId))
|
||||||
.put(APP_NAME_KEY, stringResource(R.string.app_name))
|
.put(APP_NAME_KEY, stringResource(R.string.app_name))
|
||||||
.put(EMOJI_KEY, WAVING_HAND_EMOJI)
|
.put(EMOJI_KEY, "\uD83D\uDC4B") // this hardcoded emoji might be moved to NonTranslatableConstants eventually
|
||||||
.format().toString()
|
.format().toString()
|
||||||
}
|
}
|
||||||
R.string.onboardingBubbleSessionIsEngineered -> {
|
R.string.onboardingBubbleSessionIsEngineered -> {
|
||||||
@ -149,7 +147,7 @@ internal fun LandingScreen(
|
|||||||
}
|
}
|
||||||
R.string.onboardingBubbleCreatingAnAccountIsEasy -> {
|
R.string.onboardingBubbleCreatingAnAccountIsEasy -> {
|
||||||
Phrase.from(stringResource(item.stringId))
|
Phrase.from(stringResource(item.stringId))
|
||||||
.put(EMOJI_KEY, BACKHAND_INDEX_POINTING_DOWN_EMOJI)
|
.put(EMOJI_KEY, "\uD83D\uDC47") // this hardcoded emoji might be moved to NonTranslatableConstants eventually
|
||||||
.format().toString()
|
.format().toString()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -13,6 +13,7 @@ import org.session.libsession.utilities.TextSecurePreferences
|
|||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager
|
import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager
|
||||||
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity
|
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.ui.setComposeContent
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
import org.thoughtcrime.securesms.util.start
|
import org.thoughtcrime.securesms.util.start
|
||||||
|
|
||||||
@ -45,4 +46,9 @@ class LoadAccountActivity : BaseActionBarActivity() {
|
|||||||
LoadAccountScreen(state, viewModel.qrErrors, viewModel::onChange, viewModel::onContinue, viewModel::onScanQrCode)
|
LoadAccountScreen(state, viewModel.qrErrors, viewModel::onChange, viewModel::onContinue, viewModel::onScanQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,12 @@ internal fun MessageNotificationsScreen(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit = quit)
|
if (state.showingBackWarningDialogText != null) {
|
||||||
|
OnboardingBackPressAlertDialog(dismissDialog,
|
||||||
|
textId = state.showingBackWarningDialogText,
|
||||||
|
quit = quit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||||
@ -58,14 +59,16 @@ internal class MessageNotificationsViewModel(
|
|||||||
fun onBackPressed(): Boolean = when (state) {
|
fun onBackPressed(): Boolean = when (state) {
|
||||||
is State.CreateAccount -> false
|
is State.CreateAccount -> false
|
||||||
is State.LoadAccount -> {
|
is State.LoadAccount -> {
|
||||||
_uiStates.update { it.copy(showDialog = true) }
|
_uiStates.update { it.copy(showingBackWarningDialogText = R.string.onboardingBackLoadAccount) }
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissDialog() {
|
fun dismissDialog() {
|
||||||
_uiStates.update { it.copy(showDialog = false) }
|
_uiStates.update {
|
||||||
|
it.copy(showingBackWarningDialogText = null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun quit() {
|
fun quit() {
|
||||||
@ -78,7 +81,7 @@ internal class MessageNotificationsViewModel(
|
|||||||
|
|
||||||
data class UiState(
|
data class UiState(
|
||||||
val pushEnabled: Boolean = true,
|
val pushEnabled: Boolean = true,
|
||||||
val showDialog: Boolean = false,
|
val showingBackWarningDialogText: Int? = null,
|
||||||
val clearData: Boolean = false
|
val clearData: Boolean = false
|
||||||
) {
|
) {
|
||||||
val pushDisabled get() = !pushEnabled
|
val pushDisabled get() = !pushEnabled
|
||||||
|
@ -73,7 +73,7 @@ public class Permissions {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) {
|
public PermissionsBuilder withRationaleDialog(@NonNull String message, @DrawableRes int... headers) {
|
||||||
this.rationalDialogHeader = headers;
|
this.rationalDialogHeader = headers;
|
||||||
this.rationaleDialogMessage = message;
|
this.rationaleDialogMessage = message;
|
||||||
return this;
|
return this;
|
||||||
@ -143,7 +143,7 @@ public class Permissions {
|
|||||||
|
|
||||||
if (!isInTargetSDKRange || permissionObject.hasAll(requestedPermissions)) {
|
if (!isInTargetSDKRange || permissionObject.hasAll(requestedPermissions)) {
|
||||||
executePreGrantedPermissionsRequest(request);
|
executePreGrantedPermissionsRequest(request);
|
||||||
} else if (rationaleDialogMessage != null && rationalDialogHeader != null) {
|
} else if (rationaleDialogMessage != null) {
|
||||||
executePermissionsRequestWithRationale(request);
|
executePermissionsRequestWithRationale(request);
|
||||||
} else {
|
} else {
|
||||||
executePermissionsRequest(request);
|
executePermissionsRequest(request);
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
@ -25,10 +26,13 @@ object RationaleDialog {
|
|||||||
onNegative: Runnable,
|
onNegative: Runnable,
|
||||||
@DrawableRes vararg drawables: Int
|
@DrawableRes vararg drawables: Int
|
||||||
): AlertDialog {
|
): AlertDialog {
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null)
|
var customView: View? = null
|
||||||
|
if (!drawables.isEmpty()) {
|
||||||
|
customView = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null)
|
||||||
.apply { clipToOutline = true }
|
.apply { clipToOutline = true }
|
||||||
val header = view.findViewById<ViewGroup>(R.id.header_container)
|
val header = customView.findViewById<ViewGroup>(R.id.header_container)
|
||||||
view.findViewById<TextView>(R.id.message).text = message
|
|
||||||
|
customView.findViewById<TextView>(R.id.message).text = message
|
||||||
|
|
||||||
fun addIcon(id: Int) {
|
fun addIcon(id: Int) {
|
||||||
ImageView(context).apply {
|
ImageView(context).apply {
|
||||||
@ -50,9 +54,16 @@ object RationaleDialog {
|
|||||||
|
|
||||||
drawables.firstOrNull()?.let(::addIcon)
|
drawables.firstOrNull()?.let(::addIcon)
|
||||||
drawables.drop(1).forEach { addPlus(); addIcon(it) }
|
drawables.drop(1).forEach { addPlus(); addIcon(it) }
|
||||||
|
}
|
||||||
|
|
||||||
return context.showSessionDialog {
|
return context.showSessionDialog {
|
||||||
view(view)
|
// show the generic title when there are no icons
|
||||||
|
if(customView != null){
|
||||||
|
view(customView)
|
||||||
|
} else {
|
||||||
|
title(R.string.permissionsRequired)
|
||||||
|
text(message)
|
||||||
|
}
|
||||||
button(R.string.theContinue) { onPositive.run() }
|
button(R.string.theContinue) { onPositive.run() }
|
||||||
button(R.string.notNow) { onNegative.run() }
|
button(R.string.notNow) { onNegative.run() }
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ class SettingsDialog {
|
|||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.permissionsRequired)
|
title(R.string.permissionsRequired)
|
||||||
text(message)
|
text(message)
|
||||||
button(R.string.theContinue, R.string.AccessibilityId_theContinue) {
|
button(R.string.sessionSettings, R.string.AccessibilityId_sessionSettings) {
|
||||||
context.startActivity(Permissions.getApplicationSettingsIntent(context))
|
context.startActivity(Permissions.getApplicationSettingsIntent(context))
|
||||||
}
|
}
|
||||||
cancelButton()
|
cancelButton()
|
||||||
|
@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
public class ChatsPreferenceFragment extends CorrectedPreferenceFragment {
|
||||||
private static final String TAG = ChatsPreferenceFragment.class.getSimpleName();
|
private static final String TAG = ChatsPreferenceFragment.class.getSimpleName();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -98,10 +98,10 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() {
|
|||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||||
.withPermanentDenialDialog(requireContext().getSubbedString(R.string.permissionsStorageSaveDenied, APP_NAME_KEY to getString(R.string.app_name)))
|
.withPermanentDenialDialog(requireContext().getSubbedString(R.string.permissionsStorageDeniedLegacy, APP_NAME_KEY to getString(R.string.app_name)))
|
||||||
.onAnyDenied {
|
.onAnyDenied {
|
||||||
val c = requireContext()
|
val c = requireContext()
|
||||||
val txt = c.getSubbedString(R.string.permissionsStorageSaveDenied, APP_NAME_KEY to getString(R.string.app_name))
|
val txt = c.getSubbedString(R.string.permissionsStorageDeniedLegacy, APP_NAME_KEY to getString(R.string.app_name))
|
||||||
Toast.makeText(c, txt, Toast.LENGTH_LONG).show()
|
Toast.makeText(c, txt, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
.onAllGranted {
|
.onAllGranted {
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.preferences;
|
|
||||||
|
|
||||||
|
|
||||||
import androidx.preference.ListPreference;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public abstract class ListSummaryPreferenceFragment extends CorrectedPreferenceFragment {
|
|
||||||
|
|
||||||
protected class ListSummaryListener implements Preference.OnPreferenceChangeListener {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
|
||||||
ListPreference listPref = (ListPreference) preference;
|
|
||||||
int entryIndex = Arrays.asList(listPref.getEntryValues()).indexOf(value);
|
|
||||||
|
|
||||||
listPref.setSummary(entryIndex >= 0 && entryIndex < listPref.getEntries().length
|
|
||||||
? listPref.getEntries()[entryIndex]
|
|
||||||
: getString(R.string.unknown));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void initializeListSummary(ListPreference pref) {
|
|
||||||
pref.setSummary(pref.getEntry());
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.preferences
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -11,7 +10,6 @@ import android.os.Bundle
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -19,17 +17,19 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.isNotificationsEnabled
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||||
|
import org.thoughtcrime.securesms.preferences.widgets.DropDownPreference
|
||||||
|
import java.util.Arrays
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() {
|
class NotificationsPreferenceFragment : CorrectedPreferenceFragment() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var pushRegistry: PushRegistry
|
lateinit var pushRegistry: PushRegistry
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var prefs: TextSecurePreferences
|
lateinit var prefs: TextSecurePreferences
|
||||||
|
|
||||||
@ -64,8 +64,16 @@ class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() {
|
|||||||
NotificationChannels.getMessageVibrate(requireContext())
|
NotificationChannels.getMessageVibrate(requireContext())
|
||||||
)
|
)
|
||||||
|
|
||||||
findPreference<Preference>(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceChangeListener = RingtoneSummaryListener()
|
findPreference<DropDownPreference>(TextSecurePreferences.RINGTONE_PREF)?.apply {
|
||||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceChangeListener = NotificationPrivacyListener()
|
setOnViewReady { updateRingtonePref() }
|
||||||
|
onPreferenceChangeListener = RingtoneSummaryListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
findPreference<DropDownPreference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)?.apply {
|
||||||
|
setOnViewReady { setDropDownLabel(entry) }
|
||||||
|
onPreferenceChangeListener = NotificationPrivacyListener()
|
||||||
|
}
|
||||||
|
|
||||||
findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener =
|
findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener =
|
||||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||||
NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean)
|
NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean)
|
||||||
@ -91,28 +99,18 @@ class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceClickListener =
|
|
||||||
Preference.OnPreferenceClickListener { preference: Preference ->
|
|
||||||
val listPreference = preference as ListPreference
|
|
||||||
listPreferenceDialog(requireContext(), listPreference) {
|
|
||||||
initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
initializeListSummary(findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) as ListPreference?)
|
|
||||||
|
|
||||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)!!.onPreferenceClickListener =
|
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)!!.onPreferenceClickListener =
|
||||||
Preference.OnPreferenceClickListener {
|
Preference.OnPreferenceClickListener {
|
||||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(requireContext())
|
Settings.EXTRA_CHANNEL_ID,
|
||||||
|
NotificationChannels.getMessagesChannel(requireContext())
|
||||||
)
|
)
|
||||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF))
|
|
||||||
initializeMessageVibrateSummary(findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF) as SwitchPreferenceCompat?)
|
initializeMessageVibrateSummary(findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF) as SwitchPreferenceCompat?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,54 +129,63 @@ class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() {
|
|||||||
NotificationChannels.updateMessageRingtone(requireContext(), uri)
|
NotificationChannels.updateMessageRingtone(requireContext(), uri)
|
||||||
prefs.setNotificationRingtone(uri.toString())
|
prefs.setNotificationRingtone(uri.toString())
|
||||||
}
|
}
|
||||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF))
|
updateRingtonePref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class RingtoneSummaryListener : Preference.OnPreferenceChangeListener {
|
private inner class RingtoneSummaryListener : Preference.OnPreferenceChangeListener {
|
||||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||||
|
val pref = preference as? DropDownPreference ?: return false
|
||||||
val value = newValue as? Uri
|
val value = newValue as? Uri
|
||||||
if (value == null || TextUtils.isEmpty(value.toString())) {
|
if (value == null || TextUtils.isEmpty(value.toString())) {
|
||||||
preference.setSummary(R.string.none)
|
pref.setDropDownLabel(context?.getString(R.string.none))
|
||||||
} else {
|
} else {
|
||||||
RingtoneManager.getRingtone(activity, value)
|
RingtoneManager.getRingtone(activity, value)
|
||||||
?.getTitle(activity)
|
?.getTitle(activity)
|
||||||
?.let { preference.summary = it }
|
?.let { pref.setDropDownLabel(it) }
|
||||||
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeRingtoneSummary(pref: Preference?) {
|
private fun updateRingtonePref() {
|
||||||
val listener = pref!!.onPreferenceChangeListener as RingtoneSummaryListener?
|
val pref = findPreference<Preference>(TextSecurePreferences.RINGTONE_PREF)
|
||||||
|
val listener: RingtoneSummaryListener =
|
||||||
|
(pref?.onPreferenceChangeListener) as? RingtoneSummaryListener
|
||||||
|
?: return
|
||||||
|
|
||||||
val uri = prefs.getNotificationRingtone()
|
val uri = prefs.getNotificationRingtone()
|
||||||
listener!!.onPreferenceChange(pref, uri)
|
listener.onPreferenceChange(pref, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeMessageVibrateSummary(pref: SwitchPreferenceCompat?) {
|
private fun initializeMessageVibrateSummary(pref: SwitchPreferenceCompat?) {
|
||||||
pref!!.isChecked = prefs.isNotificationVibrateEnabled()
|
pref!!.isChecked = prefs.isNotificationVibrateEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class NotificationPrivacyListener : ListSummaryListener() {
|
private inner class NotificationPrivacyListener : Preference.OnPreferenceChangeListener {
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
override fun onPreferenceChange(preference: Preference, value: Any): Boolean {
|
override fun onPreferenceChange(preference: Preference, value: Any): Boolean {
|
||||||
|
// update drop down
|
||||||
|
val pref = preference as? DropDownPreference ?: return false
|
||||||
|
val entryIndex = Arrays.asList(*pref.entryValues).indexOf(value)
|
||||||
|
|
||||||
|
pref.setDropDownLabel(
|
||||||
|
if (entryIndex >= 0 && entryIndex < pref.entries.size
|
||||||
|
) pref.entries[entryIndex]
|
||||||
|
else getString(R.string.unknown)
|
||||||
|
)
|
||||||
|
|
||||||
|
// update notification
|
||||||
object : AsyncTask<Void?, Void?, Void?>() {
|
object : AsyncTask<Void?, Void?, Void?>() {
|
||||||
override fun doInBackground(vararg params: Void?): Void? {
|
override fun doInBackground(vararg params: Void?): Void? {
|
||||||
ApplicationContext.getInstance(activity).messageNotifier.updateNotification(activity!!)
|
ApplicationContext.getInstance(activity).messageNotifier.updateNotification(
|
||||||
|
activity!!
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||||
return super.onPreferenceChange(preference, value)
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Suppress("unused")
|
|
||||||
private val TAG = NotificationsPreferenceFragment::class.java.simpleName
|
|
||||||
fun getSummary(context: Context): CharSequence = when (isNotificationsEnabled(context)) {
|
|
||||||
true -> R.string.on
|
|
||||||
false -> R.string.off
|
|
||||||
}.let(context::getString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNoti
|
|||||||
import org.thoughtcrime.securesms.util.IntentUtils
|
import org.thoughtcrime.securesms.util.IntentUtils
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
|
class PrivacySettingsPreferenceFragment : CorrectedPreferenceFragment() {
|
||||||
|
|
||||||
@Inject lateinit var configFactory: ConfigFactory
|
@Inject lateinit var configFactory: ConfigFactory
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import org.session.libsignal.utilities.PublicKeyValidation
|
|||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.database.threadDatabase
|
import org.thoughtcrime.securesms.database.threadDatabase
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
|
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
|
||||||
@ -68,6 +69,11 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@ -6,6 +6,7 @@ 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.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@ -13,136 +14,115 @@ import android.util.SparseArray
|
|||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import com.canhub.cropper.CropImage
|
|
||||||
import com.canhub.cropper.CropImageContract
|
import com.canhub.cropper.CropImageContract
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.BuildConfig
|
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 nl.komponents.kovenant.ui.alwaysUi
|
|
||||||
import nl.komponents.kovenant.ui.failUi
|
|
||||||
import nl.komponents.kovenant.ui.successUi
|
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
|
||||||
import org.session.libsession.avatars.ProfileContactPhoto
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.snode.SnodeAPI
|
|
||||||
import org.session.libsession.utilities.Address
|
|
||||||
import org.session.libsession.utilities.NonTranslatableStringConstants.DEBUG_MENU
|
|
||||||
import org.session.libsession.utilities.ProfileKeyUtil
|
|
||||||
import org.session.libsession.utilities.ProfilePictureUtilities
|
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.VERSION_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.VERSION_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.session.libsession.utilities.truncateIdForDisplay
|
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.Util.SECURE_RANDOM
|
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||||
import org.thoughtcrime.securesms.components.ProfilePictureView
|
|
||||||
import org.thoughtcrime.securesms.debugmenu.DebugActivity
|
import org.thoughtcrime.securesms.debugmenu.DebugActivity
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
|
||||||
import org.thoughtcrime.securesms.home.PathActivity
|
import org.thoughtcrime.securesms.home.PathActivity
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
|
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.*
|
||||||
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
|
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
|
||||||
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
|
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.Avatar
|
||||||
import org.thoughtcrime.securesms.ui.Cell
|
import org.thoughtcrime.securesms.ui.Cell
|
||||||
|
import org.thoughtcrime.securesms.ui.DialogButtonModel
|
||||||
import org.thoughtcrime.securesms.ui.Divider
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import org.thoughtcrime.securesms.ui.LargeItemButton
|
import org.thoughtcrime.securesms.ui.LargeItemButton
|
||||||
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
|
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
|
||||||
|
import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator
|
||||||
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import org.thoughtcrime.securesms.ui.setThemedContent
|
import org.thoughtcrime.securesms.ui.setThemedContent
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
||||||
import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
|
import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
|
||||||
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.NetworkUtils
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||||
private val TAG = "SettingsActivity"
|
private val TAG = "SettingsActivity"
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var configFactory: ConfigFactory
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var prefs: TextSecurePreferences
|
lateinit var prefs: TextSecurePreferences
|
||||||
|
|
||||||
|
private val viewModel: SettingsViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var binding: ActivitySettingsBinding
|
private lateinit var binding: ActivitySettingsBinding
|
||||||
private var displayNameEditActionMode: ActionMode? = null
|
private var displayNameEditActionMode: ActionMode? = null
|
||||||
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
||||||
private var tempFile: File? = null
|
|
||||||
|
|
||||||
private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!!
|
|
||||||
|
|
||||||
private val onAvatarCropped = registerForActivityResult(CropImageContract()) { result ->
|
private val onAvatarCropped = registerForActivityResult(CropImageContract()) { result ->
|
||||||
when {
|
viewModel.onAvatarPicked(result)
|
||||||
result.isSuccessful -> {
|
|
||||||
Log.i(TAG, result.getUriFilePath(this).toString())
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val profilePictureToBeUploaded =
|
|
||||||
BitmapUtil.createScaledBytes(
|
|
||||||
this@SettingsActivity,
|
|
||||||
result.getUriFilePath(this@SettingsActivity).toString(),
|
|
||||||
ProfileMediaConstraints()
|
|
||||||
).bitmap
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
updateProfilePicture(profilePictureToBeUploaded)
|
|
||||||
}
|
|
||||||
} catch (e: BitmapDecodingException) {
|
|
||||||
Log.e(TAG, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result is CropImage.CancelledResult -> {
|
|
||||||
Log.i(TAG, "Cropping image was cancelled by the user")
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.e(TAG, "Cropping image failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onPickImage = registerForActivityResult(
|
private val onPickImage = registerForActivityResult(
|
||||||
@ -151,12 +131,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
|
|
||||||
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
||||||
val inputFile: Uri? = result.data?.data ?: tempFile?.let(Uri::fromFile)
|
val inputFile: Uri? = result.data?.data ?: viewModel.getTempFile()?.let(Uri::fromFile)
|
||||||
cropImage(inputFile, outputFile)
|
cropImage(inputFile, outputFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage)
|
private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage)
|
||||||
|
|
||||||
|
private var showAvatarDialog: Boolean by mutableStateOf(false)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SCROLL_STATE = "SCROLL_STATE"
|
private const val SCROLL_STATE = "SCROLL_STATE"
|
||||||
}
|
}
|
||||||
@ -169,17 +151,31 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
// set the toolbar icon to a close icon
|
// set the toolbar icon to a close icon
|
||||||
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
|
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
|
||||||
|
|
||||||
|
// set the compose dialog content
|
||||||
|
binding.avatarDialog.setThemedContent {
|
||||||
|
if(showAvatarDialog){
|
||||||
|
AvatarDialogContainer(
|
||||||
|
saveAvatar = viewModel::saveAvatar,
|
||||||
|
removeAvatar = viewModel::removeAvatar,
|
||||||
|
startAvatarSelection = ::startAvatarSelection
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
binding.run {
|
binding.run {
|
||||||
setupProfilePictureView(profilePictureView)
|
profilePictureView.apply {
|
||||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
publicKey = viewModel.hexEncodedPublicKey
|
||||||
|
displayName = viewModel.getDisplayName()
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
profilePictureView.setOnClickListener {
|
||||||
|
binding.avatarDialog.isVisible = true
|
||||||
|
showAvatarDialog = true
|
||||||
|
}
|
||||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||||
btnGroupNameDisplay.text = getDisplayName()
|
btnGroupNameDisplay.text = viewModel.getDisplayName()
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
publicKeyTextView.text = viewModel.hexEncodedPublicKey
|
||||||
val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
|
val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
|
||||||
val environment: String = if(BuildConfig.BUILD_TYPE == "release") "" else " - ${prefs.getEnvironment().label}"
|
val environment: String = if(BuildConfig.BUILD_TYPE == "release") "" else " - ${prefs.getEnvironment().label}"
|
||||||
val versionDetails = " ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars) $environment"
|
val versionDetails = " ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars) $environment"
|
||||||
@ -190,6 +186,25 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.composeView.setThemedContent {
|
binding.composeView.setThemedContent {
|
||||||
Buttons()
|
Buttons()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.showLoader.collect {
|
||||||
|
binding.loader.isVisible = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.refreshAvatar.collect {
|
||||||
|
binding.profilePictureView.recycle()
|
||||||
|
binding.profilePictureView.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
binding.profilePictureView.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
@ -197,17 +212,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_bottom)
|
overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayName(): String =
|
|
||||||
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
|
|
||||||
|
|
||||||
private fun setupProfilePictureView(view: ProfilePictureView) {
|
|
||||||
view.apply {
|
|
||||||
publicKey = hexEncodedPublicKey
|
|
||||||
displayName = getDisplayName()
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
val scrollBundle = SparseArray<Parcelable>()
|
val scrollBundle = SparseArray<Parcelable>()
|
||||||
@ -291,7 +295,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
} else {
|
} else {
|
||||||
// if we have a network connection then attempt to update the display name
|
// if we have a network connection then attempt to update the display name
|
||||||
TextSecurePreferences.setProfileName(this, displayName)
|
TextSecurePreferences.setProfileName(this, displayName)
|
||||||
val user = configFactory.user
|
val user = viewModel.getUser()
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
Log.w(TAG, "Cannot update display name - missing user details from configFactory.")
|
Log.w(TAG, "Cannot update display name - missing user details from configFactory.")
|
||||||
} else {
|
} else {
|
||||||
@ -311,89 +315,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.loader.isVisible = false
|
binding.loader.isVisible = false
|
||||||
return updateWasSuccessful
|
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
val userConfig = configFactory.user
|
|
||||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
|
||||||
prefs.setProfileAvatarId(SECURE_RANDOM.nextInt() )
|
|
||||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
|
||||||
|
|
||||||
// Attempt to grab the details we require to update the profile picture
|
|
||||||
val url = prefs.getProfilePictureURL()
|
|
||||||
val profileKey = ProfileKeyUtil.getProfileKey(this)
|
|
||||||
|
|
||||||
// If we have a URL and a profile key then set the user's profile picture
|
|
||||||
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
|
|
||||||
userConfig?.setPic(UserPic(url, profileKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userConfig != null && userConfig.needsDump()) {
|
|
||||||
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
|
||||||
|
|
||||||
// Update our visuals
|
|
||||||
binding.profilePictureView.recycle()
|
|
||||||
binding.profilePictureView.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the sync failed then inform the user
|
|
||||||
updateProfilePicturePromise.failUi { onFail() }
|
|
||||||
|
|
||||||
// Finally, remove the loader animation after we've waited for the attempt to succeed or fail
|
|
||||||
updateProfilePicturePromise.alwaysUi { binding.loader.isVisible = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateProfilePicture(profilePicture: ByteArray) {
|
|
||||||
|
|
||||||
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
|
|
||||||
if (!haveNetworkConnection) {
|
|
||||||
Log.w(TAG, "Cannot update profile picture - no network connection.")
|
|
||||||
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val onFail: () -> Unit = {
|
|
||||||
Log.e(TAG, "Sync failed when uploading profile picture.")
|
|
||||||
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
syncProfilePicture(profilePicture, onFail)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeProfilePicture() {
|
|
||||||
|
|
||||||
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
|
|
||||||
if (!haveNetworkConnection) {
|
|
||||||
Log.w(TAG, "Cannot remove profile picture - no network connection.")
|
|
||||||
Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val onFail: () -> Unit = {
|
|
||||||
Log.e(TAG, "Sync failed when removing profile picture.")
|
|
||||||
Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
val emptyProfilePicture = ByteArray(0)
|
|
||||||
syncProfilePicture(emptyProfilePicture, onFail)
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
@ -417,39 +338,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
return updateDisplayName(displayName)
|
return updateDisplayName(displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEditProfilePictureUI() {
|
|
||||||
showSessionDialog {
|
|
||||||
title(R.string.profileDisplayPictureSet)
|
|
||||||
view(R.layout.dialog_change_avatar)
|
|
||||||
|
|
||||||
// Note: This is the only instance in a dialog where the "Save" button is not a `dangerButton`
|
|
||||||
button(R.string.save) { startAvatarSelection() }
|
|
||||||
|
|
||||||
if (prefs.getProfileAvatarId() != 0) {
|
|
||||||
button(R.string.remove) { removeProfilePicture() }
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}.apply {
|
|
||||||
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
|
|
||||||
?.also(::setupProfilePictureView)
|
|
||||||
|
|
||||||
val pictureIcon = findViewById<View>(R.id.ic_pictures)
|
|
||||||
|
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
|
|
||||||
|
|
||||||
val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
|
|
||||||
|
|
||||||
profilePic?.isVisible = photoSet
|
|
||||||
pictureIcon?.isVisible = !photoSet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startAvatarSelection() {
|
private fun startAvatarSelection() {
|
||||||
// Ask for an optional camera permission.
|
// Ask for an optional camera permission.
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.onAnyResult {
|
.onAnyResult {
|
||||||
tempFile = avatarSelection.startAvatarSelection( false, true)
|
avatarSelection.startAvatarSelection(
|
||||||
|
includeClear = false,
|
||||||
|
attemptToIncludeCamera = true,
|
||||||
|
createTempFile = viewModel::createTempFile
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
@ -523,7 +421,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
Column {
|
Column {
|
||||||
// add the debug menu in non release builds
|
// add the debug menu in non release builds
|
||||||
if (BuildConfig.BUILD_TYPE != "release") {
|
if (BuildConfig.BUILD_TYPE != "release") {
|
||||||
LargeItemButton(DEBUG_MENU, R.drawable.ic_settings) { push<DebugActivity>() }
|
LargeItemButton("Debug Menu", R.drawable.ic_settings) { push<DebugActivity>() }
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,6 +474,135 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AvatarDialogContainer(
|
||||||
|
startAvatarSelection: ()->Unit,
|
||||||
|
saveAvatar: ()->Unit,
|
||||||
|
removeAvatar: ()->Unit
|
||||||
|
){
|
||||||
|
val state by viewModel.avatarDialogState.collectAsState()
|
||||||
|
|
||||||
|
AvatarDialog(
|
||||||
|
state = state,
|
||||||
|
startAvatarSelection = startAvatarSelection,
|
||||||
|
saveAvatar = saveAvatar,
|
||||||
|
removeAvatar = removeAvatar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AvatarDialog(
|
||||||
|
state: SettingsViewModel.AvatarDialogState,
|
||||||
|
startAvatarSelection: ()->Unit,
|
||||||
|
saveAvatar: ()->Unit,
|
||||||
|
removeAvatar: ()->Unit
|
||||||
|
){
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.onAvatarDialogDismissed()
|
||||||
|
showAvatarDialog = false
|
||||||
|
},
|
||||||
|
title = stringResource(R.string.profileDisplayPictureSet),
|
||||||
|
content = {
|
||||||
|
// custom content that has the displayed images
|
||||||
|
|
||||||
|
// main container that control the overall size and adds the rounded bg
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = LocalDimensions.current.smallSpacing)
|
||||||
|
.size(dimensionResource(id = R.dimen.large_profile_picture_size))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null // the ripple doesn't look nice as a square with the plus icon on top too
|
||||||
|
) {
|
||||||
|
startAvatarSelection()
|
||||||
|
}
|
||||||
|
.testTag(stringResource(R.string.AccessibilityId_avatarPicker))
|
||||||
|
.background(
|
||||||
|
shape = CircleShape,
|
||||||
|
color = LocalColors.current.backgroundBubbleReceived,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
// the image content will depend on state type
|
||||||
|
when(val s = state){
|
||||||
|
// user avatar
|
||||||
|
is UserAvatar -> {
|
||||||
|
Avatar(userAddress = s.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// temporary image
|
||||||
|
is TempAvatar -> {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(dimensionResource(id = R.dimen.large_profile_picture_size))
|
||||||
|
.clip(shape = CircleShape,),
|
||||||
|
bitmap = BitmapFactory.decodeByteArray(s.data, 0, s.data.size).asImageBitmap(),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty state
|
||||||
|
else -> {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
painter = painterResource(id = R.drawable.ic_pictures),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(LocalColors.current.textSecondary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// '+' button that sits atop the custom content
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(LocalDimensions.current.spacing)
|
||||||
|
.background(
|
||||||
|
shape = CircleShape,
|
||||||
|
color = LocalColors.current.primary
|
||||||
|
)
|
||||||
|
.padding(LocalDimensions.current.xxxsSpacing)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
,
|
||||||
|
painter = painterResource(id = R.drawable.ic_plus),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(Color.Black)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showCloseButton = true, // display the 'x' button
|
||||||
|
buttons = listOf(
|
||||||
|
DialogButtonModel(
|
||||||
|
text = GetString(R.string.save),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_save),
|
||||||
|
enabled = state is TempAvatar,
|
||||||
|
onClick = saveAvatar
|
||||||
|
),
|
||||||
|
DialogButtonModel(
|
||||||
|
text = GetString(R.string.remove),
|
||||||
|
contentDescription = GetString(R.string.AccessibilityId_remove),
|
||||||
|
enabled = state is UserAvatar || // can remove is the user has an avatar set
|
||||||
|
(state is TempAvatar && state.hasAvatar),
|
||||||
|
onClick = removeAvatar
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewAvatarDialog(
|
||||||
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
|
||||||
|
){
|
||||||
|
PreviewTheme(colors) {
|
||||||
|
AvatarDialog(
|
||||||
|
state = NoAvatar,
|
||||||
|
startAvatarSelection = {},
|
||||||
|
saveAvatar = {},
|
||||||
|
removeAvatar = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.hasPaths(): Flow<Boolean> = LocalBroadcastManager.getInstance(this).hasPaths()
|
private fun Context.hasPaths(): Flow<Boolean> = LocalBroadcastManager.getInstance(this).hasPaths()
|
||||||
|
@ -0,0 +1,241 @@
|
|||||||
|
package org.thoughtcrime.securesms.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.canhub.cropper.CropImage
|
||||||
|
import com.canhub.cropper.CropImageView
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.libsession_util.util.UserPic
|
||||||
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.ProfileKeyUtil
|
||||||
|
import org.session.libsession.utilities.ProfilePictureUtilities
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
|
import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.NoExternalStorageException
|
||||||
|
import org.session.libsignal.utilities.Util.SECURE_RANDOM
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.TempAvatar
|
||||||
|
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||||
|
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||||
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
import org.thoughtcrime.securesms.util.NetworkUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettingsViewModel @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val prefs: TextSecurePreferences,
|
||||||
|
private val configFactory: ConfigFactory
|
||||||
|
) : ViewModel() {
|
||||||
|
private val TAG = "SettingsViewModel"
|
||||||
|
|
||||||
|
private var tempFile: File? = null
|
||||||
|
|
||||||
|
val hexEncodedPublicKey: String get() = prefs.getLocalNumber() ?: ""
|
||||||
|
|
||||||
|
private val userAddress = Address.fromSerialized(hexEncodedPublicKey)
|
||||||
|
|
||||||
|
private val _avatarDialogState: MutableStateFlow<AvatarDialogState> = MutableStateFlow(
|
||||||
|
getDefaultAvatarDialogState()
|
||||||
|
)
|
||||||
|
val avatarDialogState: StateFlow<AvatarDialogState>
|
||||||
|
get() = _avatarDialogState
|
||||||
|
|
||||||
|
private val _showLoader: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
val showLoader: StateFlow<Boolean>
|
||||||
|
get() = _showLoader
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the avatar on the main settings page
|
||||||
|
*/
|
||||||
|
private val _refreshAvatar: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||||
|
val refreshAvatar: SharedFlow<Unit>
|
||||||
|
get() = _refreshAvatar.asSharedFlow()
|
||||||
|
|
||||||
|
fun getDisplayName(): String =
|
||||||
|
prefs.getProfileName() ?: truncateIdForDisplay(hexEncodedPublicKey)
|
||||||
|
|
||||||
|
fun hasAvatar() = prefs.getProfileAvatarId() != 0
|
||||||
|
|
||||||
|
fun createTempFile(): File? {
|
||||||
|
try {
|
||||||
|
tempFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(context))
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("Cannot reserve a temporary avatar capture file.", e)
|
||||||
|
} catch (e: NoExternalStorageException) {
|
||||||
|
Log.e("Cannot reserve a temporary avatar capture file.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempFile
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTempFile() = tempFile
|
||||||
|
|
||||||
|
fun getUser() = configFactory.user
|
||||||
|
|
||||||
|
fun onAvatarPicked(result: CropImageView.CropResult) {
|
||||||
|
when {
|
||||||
|
result.isSuccessful -> {
|
||||||
|
Log.i(TAG, result.getUriFilePath(context).toString())
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val profilePictureToBeUploaded =
|
||||||
|
BitmapUtil.createScaledBytes(
|
||||||
|
context,
|
||||||
|
result.getUriFilePath(context).toString(),
|
||||||
|
ProfileMediaConstraints()
|
||||||
|
).bitmap
|
||||||
|
|
||||||
|
// update dialog with temporary avatar (has not been saved/uploaded yet)
|
||||||
|
_avatarDialogState.value =
|
||||||
|
AvatarDialogState.TempAvatar(profilePictureToBeUploaded, hasAvatar())
|
||||||
|
} catch (e: BitmapDecodingException) {
|
||||||
|
Log.e(TAG, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result is CropImage.CancelledResult -> {
|
||||||
|
Log.i(TAG, "Cropping image was cancelled by the user")
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Log.e(TAG, "Cropping image failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAvatarDialogDismissed() {
|
||||||
|
_avatarDialogState.value = getDefaultAvatarDialogState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultAvatarDialogState() = if (hasAvatar()) AvatarDialogState.UserAvatar(userAddress)
|
||||||
|
else AvatarDialogState.NoAvatar
|
||||||
|
|
||||||
|
fun saveAvatar() {
|
||||||
|
val tempAvatar = (avatarDialogState.value as? TempAvatar)?.data
|
||||||
|
?: return Toast.makeText(context, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
|
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(context);
|
||||||
|
if (!haveNetworkConnection) {
|
||||||
|
Log.w(TAG, "Cannot update profile picture - no network connection.")
|
||||||
|
Toast.makeText(context, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val onFail: () -> Unit = {
|
||||||
|
Log.e(TAG, "Sync failed when uploading profile picture.")
|
||||||
|
Toast.makeText(context, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
syncProfilePicture(tempAvatar, onFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun removeAvatar() {
|
||||||
|
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(context);
|
||||||
|
if (!haveNetworkConnection) {
|
||||||
|
Log.w(TAG, "Cannot remove profile picture - no network connection.")
|
||||||
|
Toast.makeText(context, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val onFail: () -> Unit = {
|
||||||
|
Log.e(TAG, "Sync failed when removing profile picture.")
|
||||||
|
Toast.makeText(context, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
val emptyProfilePicture = ByteArray(0)
|
||||||
|
syncProfilePicture(emptyProfilePicture, onFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method used by updateProfilePicture and removeProfilePicture to sync it online
|
||||||
|
private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_showLoader.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Grab the profile key and kick of the promise to update the profile picture
|
||||||
|
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(context)
|
||||||
|
ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, context)
|
||||||
|
|
||||||
|
// If the online portion of the update succeeded then update the local state
|
||||||
|
val userConfig = configFactory.user
|
||||||
|
AvatarHelper.setAvatar(
|
||||||
|
context,
|
||||||
|
Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)!!),
|
||||||
|
profilePicture
|
||||||
|
)
|
||||||
|
|
||||||
|
// When removing the profile picture the supplied ByteArray is empty so we'll clear the local data
|
||||||
|
if (profilePicture.isEmpty()) {
|
||||||
|
MessagingModuleConfiguration.shared.storage.clearUserPic()
|
||||||
|
|
||||||
|
// update dialog state
|
||||||
|
_avatarDialogState.value = AvatarDialogState.NoAvatar
|
||||||
|
} else {
|
||||||
|
prefs.setProfileAvatarId(SECURE_RANDOM.nextInt())
|
||||||
|
ProfileKeyUtil.setEncodedProfileKey(context, encodedProfileKey)
|
||||||
|
|
||||||
|
// Attempt to grab the details we require to update the profile picture
|
||||||
|
val url = prefs.getProfilePictureURL()
|
||||||
|
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||||
|
|
||||||
|
// If we have a URL and a profile key then set the user's profile picture
|
||||||
|
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
|
||||||
|
userConfig?.setPic(UserPic(url, profileKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// update dialog state
|
||||||
|
_avatarDialogState.value = AvatarDialogState.UserAvatar(userAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userConfig != null && userConfig.needsDump()) {
|
||||||
|
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||||
|
} catch (e: Exception){ // If the sync failed then inform the user
|
||||||
|
Log.d(TAG, "Error syncing avatar: $e")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onFail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally update the main avatar
|
||||||
|
_refreshAvatar.emit(Unit)
|
||||||
|
// And remove the loader animation after we've waited for the attempt to succeed or fail
|
||||||
|
_showLoader.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class AvatarDialogState() {
|
||||||
|
object NoAvatar : AvatarDialogState()
|
||||||
|
data class UserAvatar(val address: Address) : AvatarDialogState()
|
||||||
|
data class TempAvatar(
|
||||||
|
val data: ByteArray,
|
||||||
|
val hasAvatar: Boolean // true if the user has an avatar set already but is in this temp state because they are trying out a new avatar
|
||||||
|
) : AvatarDialogState()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package org.thoughtcrime.securesms.preferences.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceViewHolder
|
||||||
|
import network.loki.messenger.R
|
||||||
|
|
||||||
|
class DropDownPreference : ListPreference {
|
||||||
|
private var dropDownLabel: TextView? = null
|
||||||
|
private var clickListener: OnPreferenceClickListener? = null
|
||||||
|
private var onViewReady: (()->Unit)? = null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context?,
|
||||||
|
attrs: AttributeSet?,
|
||||||
|
defStyleAttr: Int,
|
||||||
|
defStyleRes: Int
|
||||||
|
) : super(
|
||||||
|
context!!, attrs, defStyleAttr, defStyleRes
|
||||||
|
) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||||
|
context!!, attrs, defStyleAttr
|
||||||
|
) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : super(
|
||||||
|
context!!, attrs
|
||||||
|
) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context!!) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
widgetLayoutResource = R.layout.preference_drop_down
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(view: PreferenceViewHolder) {
|
||||||
|
super.onBindViewHolder(view)
|
||||||
|
this.dropDownLabel = view.findViewById(R.id.drop_down_label) as TextView
|
||||||
|
|
||||||
|
onViewReady?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setOnPreferenceClickListener(onPreferenceClickListener: OnPreferenceClickListener?) {
|
||||||
|
this.clickListener = onPreferenceClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnViewReady(init: (()->Unit)){
|
||||||
|
this.onViewReady = init
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
if (clickListener == null || !clickListener!!.onPreferenceClick(this)) {
|
||||||
|
super.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDropDownLabel(label: CharSequence?){
|
||||||
|
dropDownLabel?.text = label
|
||||||
|
}
|
||||||
|
}
|
@ -1,70 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.preferences.widgets;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.preference.ListPreference;
|
|
||||||
import androidx.preference.PreferenceViewHolder;
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class SignalListPreference extends ListPreference {
|
|
||||||
|
|
||||||
private TextView rightSummary;
|
|
||||||
private CharSequence summary;
|
|
||||||
private OnPreferenceClickListener clickListener;
|
|
||||||
|
|
||||||
public SignalListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalListPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalListPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
setWidgetLayoutResource(R.layout.preference_right_summary_widget);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
|
||||||
super.onBindViewHolder(view);
|
|
||||||
|
|
||||||
this.rightSummary = (TextView)view.findViewById(R.id.right_summary);
|
|
||||||
setSummary(this.summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummary(CharSequence summary) {
|
|
||||||
super.setSummary(null);
|
|
||||||
|
|
||||||
this.summary = summary;
|
|
||||||
|
|
||||||
if (this.rightSummary != null) {
|
|
||||||
this.rightSummary.setText(summary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) {
|
|
||||||
this.clickListener = onPreferenceClickListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onClick() {
|
|
||||||
if (clickListener == null || !clickListener.onPreferenceClick(this)) {
|
|
||||||
super.onClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.preferences.widgets;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
import androidx.preference.PreferenceViewHolder;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class SignalPreference extends Preference {
|
|
||||||
|
|
||||||
private TextView rightSummary;
|
|
||||||
private CharSequence summary;
|
|
||||||
|
|
||||||
public SignalPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
setWidgetLayoutResource(R.layout.preference_right_summary_widget);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
|
||||||
super.onBindViewHolder(view);
|
|
||||||
|
|
||||||
this.rightSummary = (TextView)view.findViewById(R.id.right_summary);
|
|
||||||
setSummary(this.summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummary(CharSequence summary) {
|
|
||||||
super.setSummary(null);
|
|
||||||
|
|
||||||
this.summary = summary;
|
|
||||||
|
|
||||||
if (this.rightSummary != null) {
|
|
||||||
this.rightSummary.setText(summary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.qr;
|
|
||||||
|
|
||||||
public interface ScanListener {
|
|
||||||
public void onQrDataFound(String data);
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.qr;
|
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.zxing.BinaryBitmap;
|
|
||||||
import com.google.zxing.ChecksumException;
|
|
||||||
import com.google.zxing.DecodeHintType;
|
|
||||||
import com.google.zxing.FormatException;
|
|
||||||
import com.google.zxing.NotFoundException;
|
|
||||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
|
||||||
import com.google.zxing.Result;
|
|
||||||
import com.google.zxing.common.HybridBinarizer;
|
|
||||||
import com.google.zxing.qrcode.QRCodeReader;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
|
||||||
import org.thoughtcrime.securesms.components.camera.CameraView.PreviewFrame;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
public class ScanningThread extends Thread implements CameraView.PreviewCallback {
|
|
||||||
|
|
||||||
private static final String TAG = ScanningThread.class.getSimpleName();
|
|
||||||
|
|
||||||
private final QRCodeReader reader = new QRCodeReader();
|
|
||||||
private final AtomicReference<ScanListener> scanListener = new AtomicReference<>();
|
|
||||||
private final Map<DecodeHintType, String> hints = new HashMap<>();
|
|
||||||
|
|
||||||
private boolean scanning = true;
|
|
||||||
private PreviewFrame previewFrame;
|
|
||||||
|
|
||||||
public void setCharacterSet(String characterSet) {
|
|
||||||
hints.put(DecodeHintType.CHARACTER_SET, characterSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setScanListener(ScanListener scanListener) {
|
|
||||||
this.scanListener.set(scanListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPreviewFrame(@NonNull PreviewFrame previewFrame) {
|
|
||||||
try {
|
|
||||||
synchronized (this) {
|
|
||||||
this.previewFrame = previewFrame;
|
|
||||||
this.notify();
|
|
||||||
}
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
while (true) {
|
|
||||||
PreviewFrame ourFrame;
|
|
||||||
|
|
||||||
synchronized (this) {
|
|
||||||
while (scanning && previewFrame == null) {
|
|
||||||
Util.wait(this, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scanning) return;
|
|
||||||
else ourFrame = previewFrame;
|
|
||||||
|
|
||||||
previewFrame = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String data = getScannedData(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight(), ourFrame.getOrientation());
|
|
||||||
ScanListener scanListener = this.scanListener.get();
|
|
||||||
|
|
||||||
if (data != null && scanListener != null) {
|
|
||||||
scanListener.onQrDataFound(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopScanning() {
|
|
||||||
synchronized (this) {
|
|
||||||
scanning = false;
|
|
||||||
notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable String getScannedData(byte[] data, int width, int height, int orientation) {
|
|
||||||
try {
|
|
||||||
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
byte[] rotatedData = new byte[data.length];
|
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
rotatedData[x * height + height - y - 1] = data[x + y * width];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int tmp = width;
|
|
||||||
width = height;
|
|
||||||
height = tmp;
|
|
||||||
data = rotatedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(data, width, height,
|
|
||||||
0, 0, width, height,
|
|
||||||
false);
|
|
||||||
|
|
||||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
|
||||||
Result result = reader.decode(bitmap, hints);
|
|
||||||
|
|
||||||
if (result != null) return result.getText();
|
|
||||||
|
|
||||||
} catch (NullPointerException | ChecksumException | FormatException | IndexOutOfBoundsException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
} catch (NotFoundException e) {
|
|
||||||
// Thanks ZXing...
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -103,7 +103,9 @@ public final class ReactionsDialogFragment extends BottomSheetDialogFragment imp
|
|||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets());
|
ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets());
|
||||||
|
|
||||||
TabLayoutMediator mediator = new TabLayoutMediator(emojiTabs, recipientPagerView, (tab, position) -> {
|
TabLayoutMediator mediator = new TabLayoutMediator(
|
||||||
|
emojiTabs, recipientPagerView, true, false,
|
||||||
|
(tab, position) -> {
|
||||||
tab.setCustomView(R.layout.reactions_pill_large);
|
tab.setCustomView(R.layout.reactions_pill_large);
|
||||||
|
|
||||||
View customView = Objects.requireNonNull(tab.getCustomView());
|
View customView = Objects.requireNonNull(tab.getCustomView());
|
||||||
@ -120,17 +122,13 @@ public final class ReactionsDialogFragment extends BottomSheetDialogFragment imp
|
|||||||
@Override
|
@Override
|
||||||
public void onTabSelected(TabLayout.Tab tab) {
|
public void onTabSelected(TabLayout.Tab tab) {
|
||||||
View customView = tab.getCustomView();
|
View customView = tab.getCustomView();
|
||||||
TextView text = customView.findViewById(R.id.reactions_pill_count);
|
|
||||||
customView.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.reaction_pill_background_selected));
|
customView.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.reaction_pill_background_selected));
|
||||||
text.setTextColor(ThemeUtil.getThemedColor(requireContext(), R.attr.reactionsPillSelectedTextColor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabUnselected(TabLayout.Tab tab) {
|
public void onTabUnselected(TabLayout.Tab tab) {
|
||||||
View customView = tab.getCustomView();
|
View customView = tab.getCustomView();
|
||||||
TextView text = customView.findViewById(R.id.reactions_pill_count);
|
|
||||||
customView.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.reaction_pill_dialog_background));
|
customView.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.reaction_pill_dialog_background));
|
||||||
text.setTextColor(ThemeUtil.getThemedColor(requireContext(), R.attr.reactionsPillNormalTextColor));
|
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onTabReselected(TabLayout.Tab tab) {}
|
public void onTabReselected(TabLayout.Tab tab) {}
|
||||||
@ -141,21 +139,6 @@ public final class ReactionsDialogFragment extends BottomSheetDialogFragment imp
|
|||||||
|
|
||||||
private void setUpRecipientsRecyclerView() {
|
private void setUpRecipientsRecyclerView() {
|
||||||
recipientsAdapter = new ReactionViewPagerAdapter(this);
|
recipientsAdapter = new ReactionViewPagerAdapter(this);
|
||||||
|
|
||||||
recipientPagerView.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPageSelected(int position) {
|
|
||||||
recipientPagerView.post(() -> recipientsAdapter.enableNestedScrollingForPosition(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageScrollStateChanged(int state) {
|
|
||||||
if (state == ViewPager2.SCROLL_STATE_IDLE) {
|
|
||||||
recipientPagerView.requestLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
recipientPagerView.setAdapter(recipientsAdapter);
|
recipientPagerView.setAdapter(recipientsAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
|
|||||||
private fun onHide() {
|
private fun onHide() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.recoveryPasswordHidePermanently)
|
title(R.string.recoveryPasswordHidePermanently)
|
||||||
htmlText(R.string.recoveryPasswordHidePermanentlyDescription1)
|
text(R.string.recoveryPasswordHidePermanentlyDescription1)
|
||||||
dangerButton(R.string.theContinue, R.string.AccessibilityId_theContinue) { onHideConfirm() }
|
dangerButton(R.string.theContinue, R.string.AccessibilityId_theContinue) { onHideConfirm() }
|
||||||
cancelButton()
|
cancelButton()
|
||||||
}
|
}
|
||||||
|
@ -333,6 +333,8 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = recipient.isLocalNumber)
|
MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = recipient.isLocalNumber)
|
||||||
.success {
|
.success {
|
||||||
threadDb.setHasSent(threadId, true)
|
threadDb.setHasSent(threadId, true)
|
||||||
|
// add a control message for our user
|
||||||
|
storage.insertMessageRequestResponseFromYou(threadId)
|
||||||
continuation.resume(Result.success(Unit))
|
continuation.resume(Result.success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
continuation.resume(Result.failure(error))
|
continuation.resume(Result.failure(error))
|
||||||
|
@ -58,6 +58,7 @@ class DialogButtonModel(
|
|||||||
val contentDescription: GetString = text,
|
val contentDescription: GetString = text,
|
||||||
val color: Color = Color.Unspecified,
|
val color: Color = Color.Unspecified,
|
||||||
val dismissOnClick: Boolean = true,
|
val dismissOnClick: Boolean = true,
|
||||||
|
val enabled: Boolean = true,
|
||||||
val onClick: () -> Unit = {},
|
val onClick: () -> Unit = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -164,7 +165,8 @@ fun AlertDialog(
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.contentDescription(it.contentDescription())
|
.contentDescription(it.contentDescription())
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
color = it.color
|
color = it.color,
|
||||||
|
enabled = it.enabled
|
||||||
) {
|
) {
|
||||||
it.onClick()
|
it.onClick()
|
||||||
if (it.dismissOnClick) onDismissRequest()
|
if (it.dismissOnClick) onDismissRequest()
|
||||||
@ -222,16 +224,24 @@ fun DialogButton(
|
|||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
color: Color = Color.Unspecified,
|
color: Color = Color.Unspecified,
|
||||||
|
enabled: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
|
enabled = enabled,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
) {
|
) {
|
||||||
|
val textColor = if(enabled) {
|
||||||
|
color.takeOrElse { LocalColors.current.text }
|
||||||
|
} else {
|
||||||
|
LocalColors.current.disabled
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
color = color.takeOrElse { LocalColors.current.text },
|
color = textColor,
|
||||||
style = LocalType.current.large.bold(),
|
style = LocalType.current.large.bold(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
|
@ -11,8 +11,12 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
@ -45,6 +49,7 @@ import androidx.compose.ui.graphics.RectangleShape
|
|||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -60,6 +65,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.components.ProfilePictureView
|
import org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCardData
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCardData
|
||||||
@ -126,7 +132,7 @@ fun LargeItemButtonWithDrawable(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
ItemButtonWithDrawable(
|
ItemButtonWithDrawable(
|
||||||
textId, icon, modifier.heightIn(min = LocalDimensions.current.minLargeItemButtonHeight),
|
textId, icon, modifier,
|
||||||
LocalType.current.h8, colors, onClick
|
LocalType.current.h8, colors, onClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -167,8 +173,13 @@ fun LargeItemButton(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
ItemButton(
|
ItemButton(
|
||||||
textId, icon, modifier.heightIn(min = LocalDimensions.current.minLargeItemButtonHeight),
|
textId = textId,
|
||||||
LocalType.current.h8, colors, onClick
|
icon = icon,
|
||||||
|
modifier = modifier,
|
||||||
|
minHeight = LocalDimensions.current.minLargeItemButtonHeight,
|
||||||
|
textStyle = LocalType.current.h8,
|
||||||
|
colors = colors,
|
||||||
|
onClick = onClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,8 +192,13 @@ fun LargeItemButton(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
ItemButton(
|
ItemButton(
|
||||||
text, icon, modifier.heightIn(min = LocalDimensions.current.minLargeItemButtonHeight),
|
text = text,
|
||||||
LocalType.current.h8, colors, onClick
|
icon = icon,
|
||||||
|
modifier = modifier,
|
||||||
|
minHeight = LocalDimensions.current.minLargeItemButtonHeight,
|
||||||
|
textStyle = LocalType.current.h8,
|
||||||
|
colors = colors,
|
||||||
|
onClick = onClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +207,7 @@ fun ItemButton(
|
|||||||
text: String,
|
text: String,
|
||||||
icon: Int,
|
icon: Int,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
minHeight: Dp = LocalDimensions.current.minItemButtonHeight,
|
||||||
textStyle: TextStyle = LocalType.current.xl,
|
textStyle: TextStyle = LocalType.current.xl,
|
||||||
colors: ButtonColors = transparentButtonColors(),
|
colors: ButtonColors = transparentButtonColors(),
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
@ -205,6 +222,7 @@ fun ItemButton(
|
|||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
minHeight = minHeight,
|
||||||
textStyle = textStyle,
|
textStyle = textStyle,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
@ -219,6 +237,7 @@ fun ItemButton(
|
|||||||
@StringRes textId: Int,
|
@StringRes textId: Int,
|
||||||
@DrawableRes icon: Int,
|
@DrawableRes icon: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
minHeight: Dp = LocalDimensions.current.minItemButtonHeight,
|
||||||
textStyle: TextStyle = LocalType.current.xl,
|
textStyle: TextStyle = LocalType.current.xl,
|
||||||
colors: ButtonColors = transparentButtonColors(),
|
colors: ButtonColors = transparentButtonColors(),
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
@ -233,6 +252,7 @@ fun ItemButton(
|
|||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
minHeight = minHeight,
|
||||||
textStyle = textStyle,
|
textStyle = textStyle,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
@ -249,20 +269,23 @@ fun ItemButton(
|
|||||||
text: String,
|
text: String,
|
||||||
icon: @Composable BoxScope.() -> Unit,
|
icon: @Composable BoxScope.() -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
minHeight: Dp = LocalDimensions.current.minLargeItemButtonHeight,
|
||||||
textStyle: TextStyle = LocalType.current.xl,
|
textStyle: TextStyle = LocalType.current.xl,
|
||||||
colors: ButtonColors = transparentButtonColors(),
|
colors: ButtonColors = transparentButtonColors(),
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.heightIn(min = minHeight)
|
||||||
|
.padding(horizontal = LocalDimensions.current.xsSpacing),
|
||||||
colors = colors,
|
colors = colors,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxHeight()
|
||||||
.width(50.dp)
|
.aspectRatio(1f)
|
||||||
.wrapContentHeight()
|
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
@ -274,7 +297,6 @@ fun ItemButton(
|
|||||||
text,
|
text,
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = LocalDimensions.current.xsSpacing)
|
|
||||||
.align(Alignment.CenterVertically),
|
.align(Alignment.CenterVertically),
|
||||||
style = textStyle
|
style = textStyle
|
||||||
)
|
)
|
||||||
@ -371,28 +393,38 @@ fun Modifier.fadingEdges(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Divider(modifier: Modifier = Modifier, startIndent: Dp = 0.dp) {
|
fun Divider(modifier: Modifier = Modifier, startIndent: Dp = 0.dp) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = modifier.padding(horizontal = LocalDimensions.current.smallSpacing)
|
modifier = modifier
|
||||||
|
.padding(horizontal = LocalDimensions.current.smallSpacing)
|
||||||
.padding(start = startIndent),
|
.padding(start = startIndent),
|
||||||
color = LocalColors.current.borders,
|
color = LocalColors.current.borders,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO This component should be fully rebuilt in Compose at some point ~~
|
||||||
@Composable
|
@Composable
|
||||||
fun RowScope.Avatar(recipient: Recipient) {
|
fun Avatar(
|
||||||
Box(
|
recipient: Recipient,
|
||||||
modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
.width(60.dp)
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = {
|
factory = {
|
||||||
ProfilePictureView(it).apply { update(recipient) }
|
ProfilePictureView(it).apply { update(recipient) }
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.width(46.dp)
|
|
||||||
.height(46.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Avatar(
|
||||||
|
userAddress: Address,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = {
|
||||||
|
ProfilePictureView(it).apply { update(userAddress) }
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -2,9 +2,14 @@ package org.thoughtcrime.securesms.ui
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.PermissionState
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.shouldShowRationale
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
|
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
|
||||||
|
|
||||||
@ -39,3 +44,17 @@ fun ComposeView.setThemedContent(content: @Composable () -> Unit) = setContent {
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalPermissionsApi
|
||||||
|
fun PermissionState.isPermanentlyDenied(): Boolean {
|
||||||
|
return !status.shouldShowRationale && !status.isGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.findActivity(): Activity {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper) {
|
||||||
|
if (context is Activity) return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Permissions should be called in the context of an Activity")
|
||||||
|
}
|
||||||
|
@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.ui.components
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
@ -20,7 +22,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Snackbar
|
import androidx.compose.material3.Snackbar
|
||||||
@ -30,8 +31,11 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -43,10 +47,8 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import androidx.core.app.ActivityCompat
|
||||||
import com.google.accompanist.permissions.isGranted
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
import com.google.accompanist.permissions.shouldShowRationale
|
|
||||||
import com.google.zxing.BinaryBitmap
|
import com.google.zxing.BinaryBitmap
|
||||||
import com.google.zxing.ChecksumException
|
import com.google.zxing.ChecksumException
|
||||||
import com.google.zxing.FormatException
|
import com.google.zxing.FormatException
|
||||||
@ -56,18 +58,23 @@ import com.google.zxing.Result
|
|||||||
import com.google.zxing.common.HybridBinarizer
|
import com.google.zxing.common.HybridBinarizer
|
||||||
import com.google.zxing.qrcode.QRCodeReader
|
import com.google.zxing.qrcode.QRCodeReader
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
|
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.DialogButtonModel
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.findActivity
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
private const val TAG = "NewMessageFragment"
|
private const val TAG = "NewMessageFragment"
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun QRScannerScreen(
|
fun QRScannerScreen(
|
||||||
errors: Flow<String>,
|
errors: Flow<String>,
|
||||||
@ -84,31 +91,14 @@ fun QRScannerScreen(
|
|||||||
) {
|
) {
|
||||||
LocalSoftwareKeyboardController.current?.hide()
|
LocalSoftwareKeyboardController.current?.hide()
|
||||||
|
|
||||||
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
val context = LocalContext.current
|
||||||
|
val permission = Manifest.permission.CAMERA
|
||||||
|
|
||||||
if (cameraPermissionState.status.isGranted) {
|
var showCameraPermissionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(context, permission)
|
||||||
|
== PackageManager.PERMISSION_GRANTED) {
|
||||||
ScanQrCode(errors, onScan)
|
ScanQrCode(errors, onScan)
|
||||||
} else if (cameraPermissionState.status.shouldShowRationale) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.padding(horizontal = 60.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.cameraGrantAccessDenied).let { txt ->
|
|
||||||
val c = LocalContext.current
|
|
||||||
Phrase.from(txt).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString()
|
|
||||||
},
|
|
||||||
style = LocalType.current.base,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(LocalDimensions.current.spacing))
|
|
||||||
OutlineButton(
|
|
||||||
stringResource(R.string.sessionSettings),
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
|
||||||
onClick = onClickSettings
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -129,11 +119,43 @@ fun QRScannerScreen(
|
|||||||
PrimaryOutlineButton(
|
PrimaryOutlineButton(
|
||||||
stringResource(R.string.cameraGrantAccess),
|
stringResource(R.string.cameraGrantAccess),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { cameraPermissionState.run { launchPermissionRequest() } }
|
onClick = {
|
||||||
|
// NOTE: We used to use the Accompanist's way to handle permissions in compose
|
||||||
|
// but it doesn't seem to offer a solution when a user manually changes a permission
|
||||||
|
// to 'Ask every time' form the app's settings.
|
||||||
|
// So we are using our custom implementation. ONE IMPORTANT THING with this approach
|
||||||
|
// is that we need to make sure every activity where this composable is used NEED to
|
||||||
|
// implement `onRequestPermissionsResult` (see LoadAccountActivity.kt for an example)
|
||||||
|
Permissions.with(context.findActivity())
|
||||||
|
.request(permission)
|
||||||
|
.withPermanentDenialDialog(
|
||||||
|
context.getSubbedString(R.string.permissionsCameraDenied,
|
||||||
|
APP_NAME_KEY to context.getString(R.string.app_name))
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// camera permission denied permanently dialog
|
||||||
|
if(showCameraPermissionDialog){
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showCameraPermissionDialog = false },
|
||||||
|
title = stringResource(R.string.permissionsRequired),
|
||||||
|
text = context.getSubbedString(R.string.permissionsCameraDenied,
|
||||||
|
APP_NAME_KEY to context.getString(R.string.app_name)),
|
||||||
|
buttons = listOf(
|
||||||
|
DialogButtonModel(
|
||||||
|
text = GetString(stringResource(id = R.string.sessionSettings)),
|
||||||
|
onClick = onClickSettings
|
||||||
|
),
|
||||||
|
DialogButtonModel(
|
||||||
|
GetString(stringResource(R.string.cancel))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ data class Dimensions(
|
|||||||
|
|
||||||
val dividerIndent: Dp = 60.dp,
|
val dividerIndent: Dp = 60.dp,
|
||||||
val appBarHeight: Dp = 64.dp,
|
val appBarHeight: Dp = 64.dp,
|
||||||
|
val minItemButtonHeight: Dp = 50.dp,
|
||||||
val minLargeItemButtonHeight: Dp = 60.dp,
|
val minLargeItemButtonHeight: Dp = 60.dp,
|
||||||
|
|
||||||
val indicatorHeight: Dp = 4.dp,
|
val indicatorHeight: Dp = 4.dp,
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.util
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import network.loki.messenger.databinding.FragmentScanQrCodeBinding
|
|
||||||
import org.thoughtcrime.securesms.qr.ScanListener
|
|
||||||
import org.thoughtcrime.securesms.qr.ScanningThread
|
|
||||||
|
|
||||||
class ScanQRCodeFragment : Fragment() {
|
|
||||||
private lateinit var binding: FragmentScanQrCodeBinding
|
|
||||||
private val scanningThread = ScanningThread()
|
|
||||||
var scanListener: ScanListener? = null
|
|
||||||
set(value) { field = value; scanningThread.setScanListener(scanListener) }
|
|
||||||
var message: CharSequence = ""
|
|
||||||
|
|
||||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View {
|
|
||||||
binding = FragmentScanQrCodeBinding.inflate(layoutInflater, viewGroup, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
|
||||||
super.onViewCreated(view, bundle)
|
|
||||||
when (resources.configuration.orientation) {
|
|
||||||
Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL
|
|
||||||
else -> binding.overlayView.orientation = LinearLayout.VERTICAL
|
|
||||||
}
|
|
||||||
binding.messageTextView.text = message
|
|
||||||
binding.messageTextView.isVisible = message.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
binding.cameraView.onResume()
|
|
||||||
binding.cameraView.setPreviewCallback(scanningThread)
|
|
||||||
try {
|
|
||||||
scanningThread.start()
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
scanningThread.setScanListener(scanListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfiguration: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfiguration)
|
|
||||||
binding.cameraView.onPause()
|
|
||||||
when (newConfiguration.orientation) {
|
|
||||||
Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL
|
|
||||||
else -> binding.overlayView.orientation = LinearLayout.VERTICAL
|
|
||||||
}
|
|
||||||
binding.cameraView.onResume()
|
|
||||||
binding.cameraView.setPreviewCallback(scanningThread)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
this.binding.cameraView.onPause()
|
|
||||||
this.scanningThread.stopScanning()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.util
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.squareup.phrase.Phrase
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import network.loki.messenger.databinding.FragmentScanQrCodePlaceholderBinding
|
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
|
||||||
|
|
||||||
class ScanQRCodePlaceholderFragment: Fragment() {
|
|
||||||
private lateinit var binding: FragmentScanQrCodePlaceholderBinding
|
|
||||||
var delegate: ScanQRCodePlaceholderFragmentDelegate? = null
|
|
||||||
|
|
||||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View {
|
|
||||||
binding = FragmentScanQrCodePlaceholderBinding.inflate(layoutInflater, viewGroup, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
binding.grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() }
|
|
||||||
|
|
||||||
binding.needCameraPermissionsTV.text = Phrase.from(context, R.string.cameraGrantAccessQr)
|
|
||||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
|
||||||
.format()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScanQRCodePlaceholderFragmentDelegate {
|
|
||||||
fun requestCameraAccess()
|
|
||||||
}
|
|
@ -1,89 +1,27 @@
|
|||||||
package org.thoughtcrime.securesms.util
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.tbruyelle.rxpermissions2.RxPermissions
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import network.loki.messenger.R
|
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
|
||||||
import org.thoughtcrime.securesms.qr.ScanListener
|
import org.thoughtcrime.securesms.ui.createThemedComposeView
|
||||||
|
|
||||||
class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDelegate, ScanListener {
|
class ScanQRCodeWrapperFragment : Fragment() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val FRAGMENT_TAG = "ScanQRCodeWrapperFragment_FRAGMENT_TAG"
|
const val FRAGMENT_TAG = "ScanQRCodeWrapperFragment_FRAGMENT_TAG"
|
||||||
}
|
}
|
||||||
|
|
||||||
var delegate: ScanQRCodeWrapperFragmentDelegate? = null
|
var delegate: ScanQRCodeWrapperFragmentDelegate? = null
|
||||||
var message: CharSequence = ""
|
|
||||||
var enabled: Boolean = true
|
|
||||||
set(value) {
|
|
||||||
val shouldUpdate = field != value // update if value changes (view appears or disappears)
|
|
||||||
field = value
|
|
||||||
if (shouldUpdate) {
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
createThemedComposeView {
|
||||||
super.setUserVisibleHint(isVisibleToUser)
|
QRScannerScreen(emptyFlow(), onScan = {
|
||||||
enabled = isVisibleToUser
|
delegate?.handleQRCodeScanned(it)
|
||||||
}
|
})
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_scan_qr_code_wrapper, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun update() {
|
|
||||||
if (!this.isAdded) return
|
|
||||||
|
|
||||||
val fragment: Fragment
|
|
||||||
if (!enabled) {
|
|
||||||
val manager = childFragmentManager
|
|
||||||
manager.findFragmentByTag(FRAGMENT_TAG)?.let { existingFragment ->
|
|
||||||
// remove existing camera fragment (if switching back to other page)
|
|
||||||
manager.beginTransaction().remove(existingFragment).commit()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
val scanQRCodeFragment = ScanQRCodeFragment()
|
|
||||||
scanQRCodeFragment.scanListener = this
|
|
||||||
scanQRCodeFragment.message = message
|
|
||||||
fragment = scanQRCodeFragment
|
|
||||||
} else {
|
|
||||||
val scanQRCodePlaceholderFragment = ScanQRCodePlaceholderFragment()
|
|
||||||
scanQRCodePlaceholderFragment.delegate = this
|
|
||||||
fragment = scanQRCodePlaceholderFragment
|
|
||||||
}
|
|
||||||
val transaction = childFragmentManager.beginTransaction()
|
|
||||||
transaction.replace(R.id.fragmentContainer, fragment, FRAGMENT_TAG)
|
|
||||||
transaction.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun requestCameraAccess() {
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
val unused = RxPermissions(this).request(Manifest.permission.CAMERA).subscribe { isGranted ->
|
|
||||||
if (isGranted) {
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQrDataFound(data: String) {
|
|
||||||
activity?.runOnUiThread {
|
|
||||||
delegate?.handleQRCodeScanned(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.webrtc
|
package org.thoughtcrime.securesms.webrtc
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -24,6 +25,7 @@ import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.OFFER
|
|||||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER
|
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER
|
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder
|
import org.thoughtcrime.securesms.util.CallNotificationBuilder
|
||||||
import org.webrtc.IceCandidate
|
import org.webrtc.IceCandidate
|
||||||
@ -59,18 +61,16 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
|||||||
Log.i("Loki", "Contact is approved?: $approvedContact")
|
Log.i("Loki", "Contact is approved?: $approvedContact")
|
||||||
if (!approvedContact && storage.getUserPublicKey() != sender) continue
|
if (!approvedContact && storage.getUserPublicKey() != sender) continue
|
||||||
|
|
||||||
if (!textSecurePreferences.isCallNotificationsEnabled()) {
|
// if the user has not enabled voice/video calls
|
||||||
|
// or if the user has not granted audio/microphone permissions
|
||||||
|
if (
|
||||||
|
!textSecurePreferences.isCallNotificationsEnabled() ||
|
||||||
|
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO)
|
||||||
|
) {
|
||||||
Log.d("Loki","Dropping call message if call notifications disabled")
|
Log.d("Loki","Dropping call message if call notifications disabled")
|
||||||
if (nextMessage.type != PRE_OFFER) continue
|
if (nextMessage.type != PRE_OFFER) continue
|
||||||
val sentTimestamp = nextMessage.sentTimestamp ?: continue
|
val sentTimestamp = nextMessage.sentTimestamp ?: continue
|
||||||
if (textSecurePreferences.setShownCallNotification()) {
|
|
||||||
// first time call notification encountered
|
|
||||||
val notification = CallNotificationBuilder.getFirstCallNotification(context, sender)
|
|
||||||
context.getSystemService(NotificationManager::class.java).notify(CallNotificationBuilder.WEBRTC_NOTIFICATION, notification)
|
|
||||||
insertMissedCall(sender, sentTimestamp, isFirstCall = true)
|
|
||||||
} else {
|
|
||||||
insertMissedCall(sender, sentTimestamp)
|
insertMissedCall(sender, sentTimestamp)
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,15 +92,11 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertMissedCall(sender: String, sentTimestamp: Long, isFirstCall: Boolean = false) {
|
private fun insertMissedCall(sender: String, sentTimestamp: Long) {
|
||||||
val currentUserPublicKey = storage.getUserPublicKey()
|
val currentUserPublicKey = storage.getUserPublicKey()
|
||||||
if (sender == currentUserPublicKey) return // don't insert a "missed" due to call notifications disabled if it's our own sender
|
if (sender == currentUserPublicKey) return // don't insert a "missed" due to call notifications disabled if it's our own sender
|
||||||
if (isFirstCall) {
|
|
||||||
storage.insertCallMessage(sender, CallMessageType.CALL_FIRST_MISSED, sentTimestamp)
|
|
||||||
} else {
|
|
||||||
storage.insertCallMessage(sender, CallMessageType.CALL_MISSED, sentTimestamp)
|
storage.insertCallMessage(sender, CallMessageType.CALL_MISSED, sentTimestamp)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun incomingHangup(callMessage: CallMessage) {
|
private fun incomingHangup(callMessage: CallMessage) {
|
||||||
val callId = callMessage.callId ?: return
|
val callId = callMessage.callId ?: return
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:state_enabled="false" android:color="@color/gray50"/>
|
<item android:state_enabled="false" android:color="?android:textColorTertiary"/>
|
||||||
<item android:color="?prominentButtonColor"/>
|
<item android:color="?prominentButtonColor"/>
|
||||||
</selector>
|
</selector>
|
Before Width: | Height: | Size: 233 B |
Before Width: | Height: | Size: 530 B |
Before Width: | Height: | Size: 152 B |
Before Width: | Height: | Size: 362 B |
Before Width: | Height: | Size: 230 B |
Before Width: | Height: | Size: 691 B |
Before Width: | Height: | Size: 354 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 457 B |
@ -1,10 +0,0 @@
|
|||||||
<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="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"/>
|
|
||||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||||||
<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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
|
||||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||||||
<vector android:height="48dp" android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
|
||||||
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
|
|
||||||
</vector>
|
|
@ -1,6 +0,0 @@
|
|||||||
<vector android:height="48dp" android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
|
||||||
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
|
|
||||||
</vector>
|
|
12
app/src/main/res/drawable/ic_copy.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="50"
|
||||||
|
android:viewportHeight="50">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.295,11.247H18.146V6.687C18.146,4.876 19.089,3.851 20.999,3.851H29.237V13.259C29.237,15.691 30.518,16.956 32.935,16.956H41.606V33.231C41.606,35.059 40.648,36.068 38.738,36.068H35.057V39.918H39.074C43.272,39.918 45.458,37.697 45.458,33.471V17.892C45.458,15.308 44.92,13.658 43.368,12.067L33.565,2.093C32.096,0.587 30.328,0 28.065,0H20.679C16.481,0 14.295,2.218 14.295,6.448V11.247ZM32.452,12.773V5.46L40.604,13.741H33.404C32.73,13.741 32.452,13.447 32.452,12.773Z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M4.571,43.552C4.571,47.798 6.744,50 10.955,50H29.353C33.563,50 35.737,47.779 35.737,43.552V28.424C35.737,25.791 35.403,24.559 33.756,22.88L23.103,12.062C21.52,10.448 20.172,10.082 17.805,10.082H10.955C6.76,10.082 4.571,12.283 4.571,16.529V43.552ZM8.422,43.313V16.753C8.422,14.957 9.365,13.932 11.278,13.932H17.318V24.693C17.318,27.509 18.711,28.882 21.491,28.882H31.882V43.313C31.882,45.14 30.923,46.149 29.03,46.149H11.262C9.365,46.149 8.422,45.14 8.422,43.313ZM21.872,25.486C21.061,25.486 20.715,25.143 20.715,24.328V14.688L31.347,25.486H21.872Z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</vector>
|
@ -5,8 +5,8 @@
|
|||||||
android:viewportHeight="20">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:pathData="M14.414,7l3.293,-3.293a1,1 0,0 0,-1.414 -1.414L13,5.586V4a1,1 0,1 0,-2 0v4.003a0.996,0.996 0,0 0,0.617 0.921A0.997,0.997 0,0 0,12 9h4a1,1 0,1 0,0 -2h-1.586z"
|
android:pathData="M14.414,7l3.293,-3.293a1,1 0,0 0,-1.414 -1.414L13,5.586V4a1,1 0,1 0,-2 0v4.003a0.996,0.996 0,0 0,0.617 0.921A0.997,0.997 0,0 0,12 9h4a1,1 0,1 0,0 -2h-1.586z"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="?message_received_text_color"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M2,3a1,1 0,0 1,1 -1h2.153a1,1 0,0 1,0.986 0.836l0.74,4.435a1,1 0,0 1,-0.54 1.06l-1.548,0.773a11.037,11.037 0,0 0,6.105 6.105l0.774,-1.548a1,1 0,0 1,1.059 -0.54l4.435,0.74a1,1 0,0 1,0.836 0.986V17a1,1 0,0 1,-1 1h-2C7.82,18 2,12.18 2,5V3z"
|
android:pathData="M2,3a1,1 0,0 1,1 -1h2.153a1,1 0,0 1,0.986 0.836l0.74,4.435a1,1 0,0 1,-0.54 1.06l-1.548,0.773a11.037,11.037 0,0 0,6.105 6.105l0.774,-1.548a1,1 0,0 1,1.059 -0.54l4.435,0.74a1,1 0,0 1,0.836 0.986V17a1,1 0,0 1,-1 1h-2C7.82,18 2,12.18 2,5V3z"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="?message_received_text_color"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
android:viewportHeight="20">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:pathData="M2,3a1,1 0,0 1,1 -1h2.153a1,1 0,0 1,0.986 0.836l0.74,4.435a1,1 0,0 1,-0.54 1.06l-1.548,0.773a11.037,11.037 0,0 0,6.105 6.105l0.774,-1.548a1,1 0,0 1,1.059 -0.54l4.435,0.74a1,1 0,0 1,0.836 0.986V17a1,1 0,0 1,-1 1h-2C7.82,18 2,12.18 2,5V3z"
|
android:pathData="M2,3a1,1 0,0 1,1 -1h2.153a1,1 0,0 1,0.986 0.836l0.74,4.435a1,1 0,0 1,-0.54 1.06l-1.548,0.773a11.037,11.037 0,0 0,6.105 6.105l0.774,-1.548a1,1 0,0 1,1.059 -0.54l4.435,0.74a1,1 0,0 1,0.836 0.986V17a1,1 0,0 1,-1 1h-2C7.82,18 2,12.18 2,5V3z"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="?danger"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M16.707,3.293a1,1 0,0 1,0 1.414L15.414,6l1.293,1.293a1,1 0,0 1,-1.414 1.414L14,7.414l-1.293,1.293a1,1 0,1 1,-1.414 -1.414L12.586,6l-1.293,-1.293a1,1 0,0 1,1.414 -1.414L14,4.586l1.293,-1.293a1,1 0,0 1,1.414 0z"
|
android:pathData="M16.707,3.293a1,1 0,0 1,0 1.414L15.414,6l1.293,1.293a1,1 0,0 1,-1.414 1.414L14,7.414l-1.293,1.293a1,1 0,1 1,-1.414 -1.414L12.586,6l-1.293,-1.293a1,1 0,0 1,1.414 -1.414L14,4.586l1.293,-1.293a1,1 0,0 1,1.414 0z"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="?danger"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
android:viewportHeight="20">
|
android:viewportHeight="20">
|
||||||
<path
|
<path
|
||||||
android:pathData="M17.924,2.617a0.997,0.997 0,0 0,-0.215 -0.322l-0.004,-0.004A0.997,0.997 0,0 0,17 2h-4a1,1 0,1 0,0 2h1.586l-3.293,3.293a1,1 0,0 0,1.414 1.414L16,5.414V7a1,1 0,1 0,2 0V3a0.997,0.997 0,0 0,-0.076 -0.383z"
|
android:pathData="M17.924,2.617a0.997,0.997 0,0 0,-0.215 -0.322l-0.004,-0.004A0.997,0.997 0,0 0,17 2h-4a1,1 0,1 0,0 2h1.586l-3.293,3.293a1,1 0,0 0,1.414 1.414L16,5.414V7a1,1 0,1 0,2 0V3a0.997,0.997 0,0 0,-0.076 -0.383z"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="?message_received_text_color"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M2,3a1,1 0,0 1,1 -1h2.153a1,1 0,0 1,0.986 0.836l0.74,4.435a1,1 0,0 1,-0.54 1.06l-1.548,0.773a11.037,11.037 0,0 0,6.105 6.105l0.774,-1.548a1,1 0,0 1,1.059 -0.54l4.435,0.74a1,1 0,0 1,0.836 0.986V17a1,1 0,0 1,-1 1h-2C7.82,18 2,12.18 2,5V3z"
|
android:pathData="M2,3a1,1 0,0 1,1 -1h2.153a1,1 0,0 1,0.986 0.836l0.74,4.435a1,1 0,0 1,-0.54 1.06l-1.548,0.773a11.037,11.037 0,0 0,6.105 6.105l0.774,-1.548a1,1 0,0 1,1.059 -0.54l4.435,0.74a1,1 0,0 1,0.836 0.986V17a1,1 0,0 1,-1 1h-2C7.82,18 2,12.18 2,5V3z"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="?message_received_text_color"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval" />
|
|
@ -1,9 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="false">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@android:color/transparent"/>
|
||||||
|
<corners android:radius="@dimen/medium_button_corner_radius" />
|
||||||
|
<stroke
|
||||||
|
android:color="?android:textColorTertiary"
|
||||||
|
android:width="@dimen/border_thickness" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<ripple
|
||||||
android:color="?prominentButtonColor">
|
android:color="?prominentButtonColor">
|
||||||
<item>
|
<item>
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<solid android:color="?colorPrimary"/>
|
<solid android:color="@android:color/transparent"/>
|
||||||
<corners android:radius="@dimen/medium_button_corner_radius" />
|
<corners android:radius="@dimen/medium_button_corner_radius" />
|
||||||
<stroke
|
<stroke
|
||||||
android:color="?prominentButtonColor"
|
android:color="?prominentButtonColor"
|
||||||
@ -11,3 +22,6 @@
|
|||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape android:shape="rectangle"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<size android:height="22dp" android:width="3dp"/>
|
|
||||||
<solid android:color="?colorAccent"/>
|
|
||||||
</shape>
|
|
@ -209,15 +209,44 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" >
|
||||||
|
<View
|
||||||
|
android:id="@+id/quote_line"
|
||||||
|
android:layout_width="3dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?colorAccent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/quote_sender"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="?message_received_text_color"
|
||||||
|
android:text="@string/you"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginStart="@dimen/small_spacing"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/quote_line"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/quote_msg"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/quote_msg"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="?message_received_text_color"
|
android:textColor="?message_received_text_color"
|
||||||
android:text="@string/appearancePreview1"
|
android:text="@string/appearancePreview1"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:drawablePadding="12dp"
|
android:layout_marginStart="@dimen/small_spacing"
|
||||||
app:drawableLeftCompat="@drawable/quote_accent_line" />
|
app:layout_constraintStart_toEndOf="@+id/quote_line"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/quote_sender"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:text="@string/appearancePreview2"
|
android:text="@string/appearancePreview2"
|
||||||
|
@ -199,7 +199,7 @@
|
|||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:background="@drawable/rounded_rectangle"
|
android:background="@drawable/rounded_rectangle"
|
||||||
android:backgroundTint="?conversation_unread_count_indicator_background">
|
android:backgroundTint="?backgroundSecondary">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/unreadCountTextView"
|
android:id="@+id/unreadCountTextView"
|
||||||
|
@ -171,10 +171,9 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:background="@drawable/new_conversation_button_background"
|
|
||||||
android:layout_marginBottom="@dimen/new_conversation_button_bottom_offset"
|
android:layout_marginBottom="@dimen/new_conversation_button_bottom_offset"
|
||||||
android:src="@drawable/ic_plus"
|
app:rippleColor="@color/button_primary_ripple"
|
||||||
app:tint="@color/white" />
|
android:src="@drawable/ic_plus" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
@ -139,4 +139,9 @@
|
|||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
<androidx.compose.ui.platform.ComposeView
|
||||||
|
android:id="@+id/avatarDialog"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|