mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 20:37:39 +00:00
@@ -35,14 +35,12 @@ import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.session.libsession.avatars.AvatarHelper;
|
||||
import org.session.libsession.database.MessageDataProvider;
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2;
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||
import org.session.libsession.snode.SnodeModule;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
|
||||
import org.session.libsession.utilities.Device;
|
||||
import org.session.libsession.utilities.Environment;
|
||||
@@ -51,8 +49,6 @@ import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.WindowDebouncer;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||
import org.session.libsignal.utilities.HTTP;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
@@ -91,17 +87,14 @@ import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
|
||||
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.util.Broadcaster;
|
||||
import org.thoughtcrime.securesms.util.VersionDataFetcher;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
|
||||
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.PeerConnectionFactory.InitializationOptions;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -110,7 +103,6 @@ import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.EntryPoints;
|
||||
import dagger.hilt.android.HiltAndroidApp;
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import network.loki.messenger.R;
|
||||
import network.loki.messenger.libsession_util.ConfigBase;
|
||||
@@ -320,10 +312,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
super.onTerminate();
|
||||
}
|
||||
|
||||
public void initializeLocaleParser() {
|
||||
LocaleParser.Companion.configure(new LocaleParseHelper());
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
@@ -424,12 +412,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
initializeLocaleParser();
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
}
|
||||
|
||||
private static class ProviderInitializationException extends RuntimeException { }
|
||||
private void setUpPollingIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
|
@@ -17,8 +17,6 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.thoughtcrime.securesms.conversation.v2.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.ActivityUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.util.ThemeState;
|
||||
@@ -97,7 +95,6 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity(true);
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
String name = getResources().getString(R.string.app_name);
|
||||
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground);
|
||||
int color = getResources().getColor(R.color.app_icon_background);
|
||||
@@ -137,9 +134,4 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
}
|
||||
|
@@ -1,31 +1,20 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public abstract class BaseActivity extends FragmentActivity {
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
String name = getResources().getString(R.string.app_name);
|
||||
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground);
|
||||
int color = getResources().getColor(R.color.app_icon_background);
|
||||
setTaskDescription(new ActivityManager.TaskDescription(name, icon, color));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ class DeleteMediaDialog {
|
||||
iconAttribute(R.attr.dialog_alert_icon)
|
||||
title(context.resources.getQuantityString(R.plurals.deleteMessage, recordCount, recordCount))
|
||||
text(context.resources.getString(R.string.deleteMessageDescriptionEveryone))
|
||||
button(R.string.delete) { doDelete.run() }
|
||||
dangerButton(R.string.delete) { doDelete.run() }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
|
@@ -1,21 +0,0 @@
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
@@ -11,26 +11,21 @@ import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewConversationActionBarBinding
|
||||
import network.loki.messenger.databinding.ViewConversationSettingBinding
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode.AfterRead
|
||||
import org.session.libsession.LocalisedTimeUtil
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
|
||||
import org.session.libsession.utilities.modifyLayoutParams
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ConversationActionBarView @JvmOverloads constructor(
|
||||
@@ -125,7 +120,7 @@ class ConversationActionBarView @JvmOverloads constructor(
|
||||
settings += ConversationSetting(
|
||||
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
|
||||
?.let {
|
||||
context.getString(R.string.notificationsHeaderMute)
|
||||
context.getString(R.string.notificationsMuted)
|
||||
}
|
||||
?: context.getString(R.string.notificationsMuted),
|
||||
ConversationSettingType.NOTIFICATION,
|
||||
|
@@ -97,8 +97,7 @@ internal class NewMessageViewModel @Inject constructor(
|
||||
|
||||
private fun Exception.toMessage() = when (this) {
|
||||
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
|
||||
is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch)
|
||||
else -> application.getString(R.string.accountIdErrorInvalid)
|
||||
else -> application.getString(R.string.onsErrorUnableToSearch)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -710,7 +710,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
// called from onCreate
|
||||
private fun setUpInputBar() {
|
||||
binding.inputBar.isGone = viewModel.hidesInputBar()
|
||||
binding.inputBar.delegate = this
|
||||
binding.inputBarRecordingView.delegate = this
|
||||
// GIF button
|
||||
@@ -854,6 +853,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
// Conversation should be deleted now, just go back
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.inputBar.isGone = uiState.hideInputBar
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -948,11 +949,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
block(deleteThread = true)
|
||||
}
|
||||
binding.declineMessageRequestButton.setOnClickListener {
|
||||
viewModel.declineMessageRequest()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2)
|
||||
fun doDecline() {
|
||||
viewModel.declineMessageRequest()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ConversationActivityV2)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
showSessionDialog {
|
||||
title(R.string.delete)
|
||||
text(resources.getString(R.string.messageRequestsDelete))
|
||||
dangerButton(R.string.delete) { doDecline() }
|
||||
button(R.string.cancel)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1775,10 +1785,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
attachmentManager.clear()
|
||||
// Reset attachments button if needed
|
||||
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
|
||||
// Put the message in the database
|
||||
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false, null, runThreadUpdate = true)
|
||||
// Send it
|
||||
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
|
||||
|
||||
// do the heavy work in the bg
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
// Put the message in the database
|
||||
message.id = mmsDb.insertMessageOutbox(
|
||||
outgoingTextMessage,
|
||||
viewModel.threadId,
|
||||
false,
|
||||
null,
|
||||
runThreadUpdate = true
|
||||
)
|
||||
// Send it
|
||||
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
|
||||
}
|
||||
|
||||
// Send a typing stopped message
|
||||
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
|
||||
return Pair(recipient.address, sentTimestamp)
|
||||
@@ -2084,7 +2105,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
showSessionDialog {
|
||||
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||
text(resources.getString(R.string.deleteMessageDescriptionEveryone))
|
||||
button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
|
||||
dangerButton(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
|
||||
cancelButton { endActionMode() }
|
||||
}
|
||||
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
|
||||
@@ -2122,16 +2143,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
showSessionDialog {
|
||||
title(R.string.banUser)
|
||||
text(R.string.communityBanDescription)
|
||||
button(R.string.banUser) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
|
||||
dangerButton(R.string.theContinue) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
|
||||
cancelButton(::endActionMode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun banAndDeleteAll(messages: Set<MessageRecord>) {
|
||||
showSessionDialog {
|
||||
title(R.string.banUser)
|
||||
title(R.string.banDeleteAll)
|
||||
text(R.string.communityBanDeleteDescription)
|
||||
button(R.string.banUser) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
|
||||
dangerButton(R.string.theContinue) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
|
||||
cancelButton(::endActionMode)
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.util.SparseArray
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.MotionEvent
|
||||
@@ -14,33 +11,22 @@ import androidx.core.util.getOrDefault
|
||||
import androidx.core.util.set
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.min
|
||||
import com.bumptech.glide.RequestManager
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
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.showSessionDialog
|
||||
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.min
|
||||
|
||||
class ConversationAdapter(
|
||||
context: Context,
|
||||
|
@@ -9,8 +9,12 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
@@ -29,6 +33,7 @@ import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import java.util.UUID
|
||||
|
||||
@@ -65,6 +70,8 @@ class ConversationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private var communityWriteAccessJob: Job? = null
|
||||
|
||||
private var _openGroup: RetrieveOnce<OpenGroup> = RetrieveOnce {
|
||||
storage.getOpenGroup(threadId)
|
||||
}
|
||||
@@ -105,6 +112,27 @@ class ConversationViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listen to community write access updates from this point
|
||||
communityWriteAccessJob?.cancel()
|
||||
communityWriteAccessJob = viewModelScope.launch {
|
||||
OpenGroupManager.getCommunitiesWriteAccessFlow()
|
||||
.map {
|
||||
if(openGroup?.groupId != null)
|
||||
it[openGroup?.groupId]
|
||||
else null
|
||||
}
|
||||
.filterNotNull()
|
||||
.collect{
|
||||
// update our community object
|
||||
_openGroup.updateTo(openGroup?.copy(canWrite = it))
|
||||
// when we get an update on the write access of a community
|
||||
// we need to update the input text accordingly
|
||||
_uiState.update { state ->
|
||||
state.copy(hideInputBar = shouldHideInputBar())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -267,7 +295,7 @@ class ConversationViewModel(
|
||||
* - We are dealing with a contact from a community (blinded recipient) that does not allow
|
||||
* requests form community members
|
||||
*/
|
||||
fun hidesInputBar(): Boolean = openGroup?.canWrite == false ||
|
||||
fun shouldHideInputBar(): Boolean = openGroup?.canWrite == false ||
|
||||
blindedRecipient?.blocksCommunityMessageRequests == true
|
||||
|
||||
fun legacyBannerRecipient(context: Context): Recipient? = recipient?.run {
|
||||
@@ -311,7 +339,8 @@ data class UiMessage(val id: Long, val message: String)
|
||||
data class ConversationUiState(
|
||||
val uiMessages: List<UiMessage> = emptyList(),
|
||||
val isMessageRequestAccepted: Boolean? = null,
|
||||
val conversationExists: Boolean
|
||||
val conversationExists: Boolean,
|
||||
val hideInputBar: Boolean = false
|
||||
)
|
||||
|
||||
data class RetrieveOnce<T>(val retrieval: () -> T?) {
|
||||
|
@@ -18,7 +18,7 @@ class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
|
||||
title(R.string.linkPreviewsEnable)
|
||||
val txt = context.getSubbedCharSequence(R.string.linkPreviewsFirstDescription, APP_NAME_KEY to APP_NAME)
|
||||
text(txt)
|
||||
button(R.string.enable) { enable() }
|
||||
dangerButton(R.string.enable) { enable() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
@@ -19,7 +18,6 @@ import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.squareup.phrase.Phrase
|
||||
import java.io.IOException
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.leave
|
||||
@@ -31,8 +29,6 @@ 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.toHexString
|
||||
import org.thoughtcrime.securesms.MissingMicrophonePermissionDialog
|
||||
import org.thoughtcrime.securesms.media.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
||||
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||
@@ -41,12 +37,16 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
|
||||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
|
||||
import org.thoughtcrime.securesms.media.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||
import org.thoughtcrime.securesms.showMuteDialog
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.ui.findActivity
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import java.io.IOException
|
||||
|
||||
object ConversationMenuHelper {
|
||||
|
||||
@@ -183,7 +183,15 @@ object ConversationMenuHelper {
|
||||
// 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)
|
||||
|
||||
Permissions.with(context.findActivity())
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.withPermanentDenialDialog(
|
||||
context.getSubbedString(R.string.permissionsMicrophoneAccessRequired,
|
||||
APP_NAME_KEY to context.getString(R.string.app_name))
|
||||
)
|
||||
.execute()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -18,10 +18,10 @@ import network.loki.messenger.databinding.ViewControlMessageBinding
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_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.expiryMode
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@@ -29,6 +29,7 @@ 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.findActivity
|
||||
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
import javax.inject.Inject
|
||||
@@ -133,15 +134,6 @@ class ControlMessageView : LinearLayout {
|
||||
// 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) -> {
|
||||
@@ -168,6 +160,38 @@ class ControlMessageView : LinearLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're currently missing the audio/microphone permission,
|
||||
// show a dedicated permission dialog
|
||||
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) -> {
|
||||
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.callsMicrophonePermissionsRequired,
|
||||
NAME_KEY to message.individualRecipient.name!!
|
||||
)
|
||||
text(bodyTxt)
|
||||
|
||||
button(R.string.theContinue) {
|
||||
Permissions.with(context.findActivity())
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.withPermanentDenialDialog(
|
||||
context.getSubbedString(R.string.permissionsMicrophoneAccessRequired,
|
||||
APP_NAME_KEY to context.getString(R.string.app_name))
|
||||
)
|
||||
.execute()
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -254,7 +254,7 @@ public class AttachmentManager {
|
||||
.request(Manifest.permission.READ_MEDIA_IMAGES)
|
||||
.request(Manifest.permission.READ_MEDIA_AUDIO)
|
||||
.withRationaleDialog(
|
||||
Phrase.from(c, R.string.permissionsStorageSend)
|
||||
Phrase.from(c, R.string.permissionsMusicAudio)
|
||||
.put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString()
|
||||
)
|
||||
.withPermanentDenialDialog(
|
||||
|
@@ -9,6 +9,7 @@ import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
@@ -27,7 +28,10 @@ import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.RequestManager
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.ui.afterMeasured
|
||||
import java.lang.Float.min
|
||||
|
||||
open class ThumbnailView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -114,8 +118,23 @@ open class ThumbnailView @JvmOverloads constructor(
|
||||
isPreview: Boolean, naturalWidth: Int,
|
||||
naturalHeight: Int
|
||||
): ListenableFuture<Boolean> {
|
||||
binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
||||
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
val showPlayOverlay = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
||||
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
if(showPlayOverlay) {
|
||||
binding.playOverlay.isVisible = true
|
||||
// The views are poorly constructed at the moment and there is no good way to know
|
||||
// if this is used in the main conversation or in the tiny quote window of a reply...
|
||||
// But when the view is too small the 'play' icon does not scale,
|
||||
// so we can do it based on measured sizes here
|
||||
binding.playOverlay.afterMeasured {
|
||||
// max size if 60% of the width
|
||||
val ratio = min((binding.root.width * 0.6f) / binding.playOverlay.width, 1f)
|
||||
binding.playOverlay.scaleX = ratio
|
||||
binding.playOverlay.scaleY = ratio
|
||||
}
|
||||
} else {
|
||||
binding.playOverlay.isVisible = false
|
||||
}
|
||||
|
||||
if (equals(this.slide, slide)) {
|
||||
// don't re-load slide
|
||||
|
@@ -1,236 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.RelativeDay;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMediaLoader.BucketedThreadMedia> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = BucketedThreadMediaLoader.class.getSimpleName();
|
||||
|
||||
private final Address address;
|
||||
private final ContentObserver observer;
|
||||
|
||||
public BucketedThreadMediaLoader(@NonNull Context context, @NonNull Address address) {
|
||||
super(context);
|
||||
this.address = address;
|
||||
this.observer = new ForceLoadContentObserver();
|
||||
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (takeContentChanged()) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAbandon() {
|
||||
DatabaseComponent.get(getContext()).mediaDatabase().unsubscribeToMediaChanges(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BucketedThreadMedia loadInBackground() {
|
||||
BucketedThreadMedia result = new BucketedThreadMedia(getContext());
|
||||
long threadId = DatabaseComponent.get(getContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(getContext(), address, true));
|
||||
|
||||
MediaDatabase mediaDatabase = DatabaseComponent.get(getContext()).mediaDatabase();
|
||||
|
||||
mediaDatabase.subscribeToMediaChanges(observer);
|
||||
try (Cursor cursor = mediaDatabase.getGalleryMediaForThread(threadId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
result.add(MediaDatabase.MediaRecord.from(getContext(), cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class BucketedThreadMedia {
|
||||
|
||||
private final TimeBucket TODAY;
|
||||
private final TimeBucket YESTERDAY;
|
||||
private final TimeBucket THIS_WEEK;
|
||||
private final TimeBucket THIS_MONTH;
|
||||
private final MonthBuckets OLDER;
|
||||
|
||||
private final TimeBucket[] TIME_SECTIONS;
|
||||
|
||||
public BucketedThreadMedia(@NonNull Context context) {
|
||||
String localisedTodayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.TODAY);
|
||||
String localisedYesterdayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.YESTERDAY);
|
||||
|
||||
this.TODAY = new TimeBucket(localisedTodayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
|
||||
this.YESTERDAY = new TimeBucket(localisedYesterdayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
|
||||
this.THIS_WEEK = new TimeBucket(context.getString(R.string.attachmentsThisWeek), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
|
||||
this.THIS_MONTH = new TimeBucket(context.getString(R.string.attachmentsThisMonth), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7));
|
||||
this.TIME_SECTIONS = new TimeBucket[] { TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH };
|
||||
this.OLDER = new MonthBuckets();
|
||||
}
|
||||
|
||||
public void add(MediaDatabase.MediaRecord mediaRecord) {
|
||||
for (TimeBucket timeSection : TIME_SECTIONS) {
|
||||
if (timeSection.inRange(mediaRecord.getDate())) {
|
||||
timeSection.add(mediaRecord);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OLDER.add(mediaRecord);
|
||||
}
|
||||
|
||||
public int getSectionCount() {
|
||||
return (int)Stream.of(TIME_SECTIONS)
|
||||
.filter(timeBucket -> !timeBucket.isEmpty())
|
||||
.count() +
|
||||
OLDER.getSectionCount();
|
||||
}
|
||||
|
||||
public int getSectionItemCount(int section) {
|
||||
List<TimeBucket> activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
|
||||
|
||||
if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItemCount();
|
||||
else return OLDER.getSectionItemCount(section - activeTimeBuckets.size());
|
||||
}
|
||||
|
||||
public MediaDatabase.MediaRecord get(int section, int item) {
|
||||
List<TimeBucket> activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
|
||||
|
||||
if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItem(item);
|
||||
else return OLDER.getItem(section - activeTimeBuckets.size(), item);
|
||||
}
|
||||
|
||||
public String getName(int section, Locale locale) {
|
||||
List<TimeBucket> activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
|
||||
|
||||
if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getName();
|
||||
else return OLDER.getName(section - activeTimeBuckets.size(), locale);
|
||||
}
|
||||
|
||||
private static class TimeBucket {
|
||||
|
||||
private final List<MediaDatabase.MediaRecord> records = new LinkedList<>();
|
||||
|
||||
private final long startTime;
|
||||
private final long endtime;
|
||||
private final String name;
|
||||
|
||||
TimeBucket(String name, long startTime, long endtime) {
|
||||
this.name = name;
|
||||
this.startTime = startTime;
|
||||
this.endtime = endtime;
|
||||
}
|
||||
|
||||
void add(MediaDatabase.MediaRecord record) {
|
||||
this.records.add(record);
|
||||
}
|
||||
|
||||
boolean inRange(long timestamp) {
|
||||
return timestamp > startTime && timestamp <= endtime;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return records.isEmpty();
|
||||
}
|
||||
|
||||
int getItemCount() {
|
||||
return records.size();
|
||||
}
|
||||
|
||||
MediaDatabase.MediaRecord getItem(int position) {
|
||||
return records.get(position);
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
static long addToCalendar(int field, int amount) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(field, amount);
|
||||
return calendar.getTimeInMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private static class MonthBuckets {
|
||||
|
||||
private final Map<Date, List<MediaDatabase.MediaRecord>> months = new HashMap<>();
|
||||
|
||||
void add(MediaDatabase.MediaRecord record) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(record.getDate());
|
||||
|
||||
int year = calendar.get(Calendar.YEAR) - 1900;
|
||||
int month = calendar.get(Calendar.MONTH);
|
||||
Date date = new Date(year, month, 1);
|
||||
|
||||
if (months.containsKey(date)) {
|
||||
months.get(date).add(record);
|
||||
} else {
|
||||
List<MediaDatabase.MediaRecord> list = new LinkedList<>();
|
||||
list.add(record);
|
||||
months.put(date, list);
|
||||
}
|
||||
}
|
||||
|
||||
int getSectionCount() {
|
||||
return months.size();
|
||||
}
|
||||
|
||||
int getSectionItemCount(int section) {
|
||||
return months.get(getSection(section)).size();
|
||||
}
|
||||
|
||||
MediaDatabase.MediaRecord getItem(int section, int position) {
|
||||
return months.get(getSection(section)).get(position);
|
||||
}
|
||||
|
||||
Date getSection(int section) {
|
||||
ArrayList<Date> keys = new ArrayList<>(months.keySet());
|
||||
Collections.sort(keys, Collections.reverseOrder());
|
||||
|
||||
return keys.get(section);
|
||||
}
|
||||
|
||||
String getName(int section, Locale locale) {
|
||||
Date sectionDate = getSection(section);
|
||||
|
||||
return new SimpleDateFormat("MMMM, yyyy", locale).format(sectionDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -99,24 +99,6 @@ public class ThreadRecord extends DisplayRecord {
|
||||
return name;
|
||||
}
|
||||
|
||||
private String getDisappearingMsgExpiryTypeString(Context context) {
|
||||
MessageRecord lm = this.lastMessage;
|
||||
if (lm == null) {
|
||||
Log.w("ThreadRecord", "Could not get last message to determine disappearing msg type.");
|
||||
return "Unknown";
|
||||
}
|
||||
long expireStarted = lm.getExpireStarted();
|
||||
|
||||
// Note: This works because expireStarted is 0 for messages which are 'Disappear after read'
|
||||
// while it's a touch higher than the sent timestamp for "Disappear after send". We could then
|
||||
// use `expireStarted == 0`, but that's not how it's done in UpdateMessageBuilder so to keep
|
||||
// things the same I'll assume there's a reason for this and follow suit.
|
||||
// Also: `this.lastMessage.getExpiresIn()` is available.
|
||||
if (expireStarted >= dateSent) {
|
||||
return context.getString(R.string.disappearingMessagesSent);
|
||||
}
|
||||
return context.getString(R.string.read);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getDisplayBody(@NonNull Context context) {
|
||||
@@ -144,22 +126,13 @@ public class ThreadRecord extends DisplayRecord {
|
||||
.put(NAME_KEY, getName())
|
||||
.format().toString();
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
|
||||
int seconds = (int) (getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return Phrase.from(context, R.string.disappearingMessagesTurnedOff)
|
||||
.put(NAME_KEY, getName())
|
||||
.format().toString();
|
||||
// Use the same message as we would for displaying on the conversation screen.
|
||||
// lastMessage shouldn't be null here, but we'll check just in case.
|
||||
if (lastMessage != null) {
|
||||
return lastMessage.getDisplayBody(context).toString();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Implied that disappearing messages is enabled..
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
String disappearAfterWhat = getDisappearingMsgExpiryTypeString(context); // Disappear after send or read?
|
||||
return Phrase.from(context, R.string.disappearingMessagesSet)
|
||||
.put(NAME_KEY, getName())
|
||||
.put(TIME_KEY, time)
|
||||
.put(DISAPPEARING_MESSAGES_TYPE_KEY, disappearAfterWhat)
|
||||
.format().toString();
|
||||
|
||||
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
|
||||
return Phrase.from(context, R.string.attachmentsMediaSaved)
|
||||
.put(NAME_KEY, getName())
|
||||
|
@@ -4,6 +4,9 @@ import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.squareup.phrase.Phrase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.util.concurrent.Executors
|
||||
import network.loki.messenger.R
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
@@ -39,6 +42,9 @@ object OpenGroupManager {
|
||||
return true
|
||||
}
|
||||
|
||||
// flow holding information on write access for our current communities
|
||||
private val _communityWriteAccess: MutableStateFlow<Map<String, Boolean>> = MutableStateFlow(emptyMap())
|
||||
|
||||
fun startPolling() {
|
||||
if (isPolling) { return }
|
||||
isPolling = true
|
||||
@@ -66,6 +72,8 @@ object OpenGroupManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun getCommunitiesWriteAccessFlow() = _communityWriteAccess.asStateFlow()
|
||||
|
||||
@WorkerThread
|
||||
fun add(server: String, room: String, publicKey: String, context: Context): Pair<Long,OpenGroupApi.RoomInfo?> {
|
||||
val openGroupID = "$server.$room"
|
||||
@@ -164,9 +172,13 @@ object OpenGroupManager {
|
||||
|
||||
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
|
||||
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||
val openGroupID = "${openGroup.server}.${openGroup.room}"
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroup.groupId, context)
|
||||
threadDB.setOpenGroupChat(openGroup, threadID)
|
||||
|
||||
// update write access for this community
|
||||
val writeAccesses = _communityWriteAccess.value.toMutableMap()
|
||||
writeAccesses[openGroup.groupId] = openGroup.canWrite
|
||||
_communityWriteAccess.value = writeAccesses
|
||||
}
|
||||
|
||||
fun isUserModerator(context: Context, groupId: String, standardPublicKey: String, blindedPublicKey: String? = null): Boolean {
|
||||
|
@@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ui.SessionShieldIcon
|
||||
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||
import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton
|
||||
import org.thoughtcrime.securesms.ui.contentDescription
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
@@ -60,8 +61,8 @@ internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
|
||||
style = LocalType.current.small
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(LocalDimensions.current.xsSpacing))
|
||||
SlimPrimaryOutlineButton(
|
||||
Spacer(Modifier.width(LocalDimensions.current.smallSpacing))
|
||||
PrimaryOutlineButton(
|
||||
text = stringResource(R.string.theContinue),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
|
@@ -1,7 +1,10 @@
|
||||
package org.thoughtcrime.securesms.media
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.RelativeDay
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.WeekFields
|
||||
import java.util.Locale
|
||||
@@ -29,16 +32,15 @@ class FixedTimeBuckets(
|
||||
)
|
||||
|
||||
/**
|
||||
* Test the given time against the buckets and return the appropriate string resource the time
|
||||
* Test the given time against the buckets and return the appropriate string the time
|
||||
* bucket. If no bucket is appropriate, it will return null.
|
||||
*/
|
||||
@StringRes
|
||||
fun getBucketText(time: ZonedDateTime): Int? {
|
||||
fun getBucketText(context: Context, time: ZonedDateTime): String? {
|
||||
return when {
|
||||
time >= startOfToday -> R.string.BucketedThreadMedia_Today
|
||||
time >= startOfYesterday -> R.string.BucketedThreadMedia_Yesterday
|
||||
time >= startOfThisWeek -> R.string.attachmentsThisWeek
|
||||
time >= startOfThisMonth -> R.string.attachmentsThisMonth
|
||||
time >= startOfToday -> DateUtils.getLocalisedRelativeDayString(RelativeDay.TODAY)
|
||||
time >= startOfYesterday -> DateUtils.getLocalisedRelativeDayString(RelativeDay.YESTERDAY)
|
||||
time >= startOfThisWeek -> context.getString(R.string.attachmentsThisWeek)
|
||||
time >= startOfThisMonth -> context.getString(R.string.attachmentsThisMonth)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@@ -247,12 +247,7 @@ private fun DeleteConfirmationDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = context.resources.getQuantityString(
|
||||
R.plurals.ConversationFragment_delete_selected_messages, numSelected
|
||||
),
|
||||
text = context.resources.getQuantityString(
|
||||
R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages,
|
||||
numSelected,
|
||||
numSelected,
|
||||
R.plurals.deleteMessage, numSelected
|
||||
),
|
||||
buttons = listOf(
|
||||
DialogButtonModel(GetString(R.string.delete), color = LocalColors.current.danger, onClick = onAccepted),
|
||||
|
@@ -130,7 +130,7 @@ class MediaOverviewViewModel(
|
||||
.groupBy { record ->
|
||||
val time =
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(record.date), ZoneId.of("UTC"))
|
||||
timeBuckets.getBucketText(time)?.let(application::getString)
|
||||
timeBuckets.getBucketText(application, time)
|
||||
?: time.toLocalDate().withDayOfMonth(1)
|
||||
}
|
||||
.map { (bucket, records) ->
|
||||
@@ -171,6 +171,11 @@ class MediaOverviewViewModel(
|
||||
|
||||
fun onItemClicked(item: MediaOverviewItem) {
|
||||
if (inSelectionMode.value) {
|
||||
if (item.slide.hasDocument()) {
|
||||
// We don't support selecting documents in selection mode
|
||||
return
|
||||
}
|
||||
|
||||
val newSet = mutableSelectedItemIDs.value.toMutableSet()
|
||||
if (item.id in newSet) {
|
||||
newSet.remove(item.id)
|
||||
@@ -213,11 +218,6 @@ class MediaOverviewViewModel(
|
||||
}
|
||||
|
||||
fun onTabItemClicked(tab: MediaOverviewTab) {
|
||||
if (inSelectionMode.value) {
|
||||
// Not allowing to switch tabs while in selection mode
|
||||
return
|
||||
}
|
||||
|
||||
mutableSelectedTab.value = tab
|
||||
}
|
||||
|
||||
|
@@ -108,7 +108,7 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
|
||||
showSessionDialog {
|
||||
title(R.string.delete)
|
||||
text(resources.getString(R.string.messageRequestsDelete))
|
||||
button(R.string.delete) { doDecline() }
|
||||
dangerButton(R.string.delete) { doDecline() }
|
||||
button(R.string.cancel)
|
||||
}
|
||||
}
|
||||
@@ -129,9 +129,10 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
|
||||
}
|
||||
|
||||
showSessionDialog {
|
||||
title(resources.getString(R.string.clearAll))
|
||||
text(resources.getString(R.string.messageRequestsClearAllExplanation))
|
||||
button(R.string.yes) { doDeleteAllAndBlock() }
|
||||
button(R.string.no)
|
||||
dangerButton(R.string.clear) { doDeleteAllAndBlock() }
|
||||
button(R.string.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -65,7 +65,7 @@ object RationaleDialog {
|
||||
text(message)
|
||||
}
|
||||
button(R.string.theContinue) { onPositive.run() }
|
||||
button(R.string.notNow) { onNegative.run() }
|
||||
button(R.string.cancel) { onNegative.run() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,11 +3,14 @@ package org.thoughtcrime.securesms.preferences
|
||||
import android.Manifest
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.Preference
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setBooleanPreference
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
|
||||
internal class CallToggleListener(
|
||||
private val context: Fragment,
|
||||
@@ -39,6 +42,10 @@ internal class CallToggleListener(
|
||||
)
|
||||
setCallback(true)
|
||||
}
|
||||
.withPermanentDenialDialog(
|
||||
context.requireContext().getSubbedString(R.string.permissionsMicrophoneAccessRequired,
|
||||
APP_NAME_KEY to context.requireContext().getString(R.string.app_name)
|
||||
))
|
||||
.onAnyDenied { setCallback(false) }
|
||||
.execute()
|
||||
}
|
||||
|
@@ -45,7 +45,6 @@ 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
|
||||
@@ -58,11 +57,9 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.canhub.cropper.CropImageContract
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -80,7 +77,9 @@ import org.thoughtcrime.securesms.debugmenu.DebugActivity
|
||||
import org.thoughtcrime.securesms.home.PathActivity
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.*
|
||||
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.NoAvatar
|
||||
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.TempAvatar
|
||||
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.UserAvatar
|
||||
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
|
||||
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
|
||||
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||
@@ -91,10 +90,10 @@ import org.thoughtcrime.securesms.ui.Divider
|
||||
import org.thoughtcrime.securesms.ui.GetString
|
||||
import org.thoughtcrime.securesms.ui.LargeItemButton
|
||||
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.PrimaryOutlineCopyButton
|
||||
import org.thoughtcrime.securesms.ui.contentDescription
|
||||
import org.thoughtcrime.securesms.ui.qaTag
|
||||
import org.thoughtcrime.securesms.ui.setThemedContent
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||
@@ -518,7 +517,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
) {
|
||||
startAvatarSelection()
|
||||
}
|
||||
.testTag(stringResource(R.string.AccessibilityId_avatarPicker))
|
||||
.qaTag(stringResource(R.string.AccessibilityId_avatarPicker))
|
||||
.background(
|
||||
shape = CircleShape,
|
||||
color = LocalColors.current.backgroundBubbleReceived,
|
||||
@@ -581,6 +580,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
DialogButtonModel(
|
||||
text = GetString(R.string.remove),
|
||||
contentDescription = GetString(R.string.AccessibilityId_remove),
|
||||
color = LocalColors.current.danger,
|
||||
enabled = state is UserAvatar || // can remove is the user has an avatar set
|
||||
(state is TempAvatar && state.hasAvatar),
|
||||
onClick = removeAvatar
|
||||
|
@@ -3,8 +3,15 @@ package org.thoughtcrime.securesms.ui
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
@@ -58,3 +65,26 @@ fun Context.findActivity(): Activity {
|
||||
}
|
||||
throw IllegalStateException("Permissions should be called in the context of an Activity")
|
||||
}
|
||||
|
||||
inline fun <T : View> T.afterMeasured(crossinline block: T.() -> Unit) {
|
||||
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
if (measuredWidth > 0 && measuredHeight > 0) {
|
||||
viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
block()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to set the test tag that the QA team can use to retrieve an element in appium
|
||||
* In order to do so we need to set the testTagsAsResourceId to true, which ideally should be done only once
|
||||
* in the root composable, but our app is currently made up of multiple isolated composables
|
||||
* set up in the old activity/fragment view system
|
||||
* As such we need to repeat it for every component that wants to use testTag, until such
|
||||
* a time as we have one root composable
|
||||
*/
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun Modifier.qaTag(tag: String) = semantics { testTagsAsResourceId = true }.testTag(tag)
|
@@ -49,6 +49,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.ChecksumException
|
||||
import com.google.zxing.FormatException
|
||||
@@ -75,6 +78,7 @@ import java.util.concurrent.Executors
|
||||
|
||||
private const val TAG = "NewMessageFragment"
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun QRScannerScreen(
|
||||
errors: Flow<String>,
|
||||
@@ -93,11 +97,11 @@ fun QRScannerScreen(
|
||||
|
||||
val context = LocalContext.current
|
||||
val permission = Manifest.permission.CAMERA
|
||||
val cameraPermissionState = rememberPermissionState(permission)
|
||||
|
||||
var showCameraPermissionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (ContextCompat.checkSelfPermission(context, permission)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
if (cameraPermissionState.status.isGranted) {
|
||||
ScanQrCode(errors, onScan)
|
||||
} else {
|
||||
Column(
|
||||
|
@@ -100,8 +100,7 @@ fun SessionOutlinedTextField(
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 28.dp)
|
||||
.padding(horizontal = 21.dp)
|
||||
.padding(LocalDimensions.current.spacing)
|
||||
) {
|
||||
if (text.isEmpty()) {
|
||||
Text(
|
||||
|
@@ -52,9 +52,6 @@ object DateUtils : android.text.format.DateUtils() {
|
||||
return isToday(`when` + TimeUnit.DAYS.toMillis(1))
|
||||
}
|
||||
|
||||
private fun convertDelta(millis: Long, to: TimeUnit): Int {
|
||||
return to.convert(System.currentTimeMillis() - millis, TimeUnit.MILLISECONDS).toInt()
|
||||
}
|
||||
|
||||
// Method to get the String for a relative day in a locale-aware fashion
|
||||
public fun getLocalisedRelativeDayString(relativeDay: RelativeDay): String {
|
||||
@@ -76,10 +73,12 @@ object DateUtils : android.text.format.DateUtils() {
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
||||
return getRelativeTimeSpanString(comparisonTime.timeInMillis,
|
||||
val temp = getRelativeTimeSpanString(
|
||||
comparisonTime.timeInMillis,
|
||||
now.timeInMillis,
|
||||
DAY_IN_MILLIS,
|
||||
FORMAT_SHOW_DATE).toString()
|
||||
return temp
|
||||
}
|
||||
|
||||
fun getFormattedDateTime(time: Long, template: String, locale: Locale): String {
|
||||
@@ -142,30 +141,4 @@ object DateUtils : android.text.format.DateUtils() {
|
||||
private fun getLocalizedPattern(template: String, locale: Locale): String {
|
||||
return DateFormat.getBestDateTimePattern(locale, template)
|
||||
}
|
||||
|
||||
/**
|
||||
* e.g. 2020-09-04T19:17:51Z
|
||||
* https://www.iso.org/iso-8601-date-and-time-format.html
|
||||
*
|
||||
* @return The timestamp if able to be parsed, otherwise -1.
|
||||
*/
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@JvmStatic
|
||||
public fun parseIso8601(date: String?): Long {
|
||||
|
||||
if (date.isNullOrEmpty()) { return -1 }
|
||||
|
||||
val format = if (Build.VERSION.SDK_INT >= 24) {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault())
|
||||
} else {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
|
||||
}
|
||||
|
||||
try {
|
||||
return format.parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
Log.w(TAG, "Failed to parse date.", e)
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util.dynamiclanguage
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import network.loki.messenger.BuildConfig
|
||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParserHelperProtocol
|
||||
import java.util.*
|
||||
|
||||
class LocaleParseHelper: LocaleParserHelperProtocol {
|
||||
|
||||
override fun appSupportsTheExactLocale(locale: Locale?): Boolean {
|
||||
return if (locale == null) {
|
||||
false
|
||||
} else Arrays.asList(*BuildConfig.LANGUAGES).contains(locale.toString())
|
||||
}
|
||||
|
||||
override fun findBestSystemLocale(): Locale {
|
||||
val config = Resources.getSystem().configuration
|
||||
|
||||
val firstMatch = ConfigurationCompat.getLocales(config)
|
||||
.getFirstMatch(BuildConfig.LANGUAGES)
|
||||
|
||||
return firstMatch ?: Locale.ENGLISH
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/default_dialog_background"
|
||||
android:insetRight="24dp"
|
||||
android:insetLeft="24dp"> <!-- Using Material's inset size -->
|
||||
</inset>
|
@@ -5,6 +5,4 @@
|
||||
|
||||
<solid android:color="?backgroundSecondary" />
|
||||
|
||||
<corners android:radius="?dialogCornerRadius" />
|
||||
|
||||
</shape>
|
@@ -45,6 +45,7 @@
|
||||
android:layout_marginEnd="64dp"
|
||||
android:background="@null"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLength="@integer/max_input_chars"
|
||||
android:hint="@string/message"
|
||||
android:textColorHint="?attr/input_bar_text_hint"
|
||||
android:textColor="?input_bar_text_user"
|
||||
|
@@ -30,7 +30,7 @@
|
||||
<dimen name="text_view_corner_radius">8dp</dimen>
|
||||
<dimen name="fake_chat_view_bubble_corner_radius">10dp</dimen>
|
||||
<dimen name="setting_button_height">64dp</dimen>
|
||||
<dimen name="dialog_corner_radius">8dp</dimen>
|
||||
<dimen name="dialog_corner_radius">12dp</dimen>
|
||||
<dimen name="video_inset_radius">11dp</dimen>
|
||||
<dimen name="pn_option_corner_radius">8dp</dimen>
|
||||
<dimen name="path_status_view_size">8dp</dimen>
|
||||
|
@@ -7,4 +7,5 @@
|
||||
|
||||
<integer name="max_user_nickname_length_chars">35</integer>
|
||||
<integer name="max_group_and_community_name_length_chars">35</integer>
|
||||
<integer name="max_input_chars">2000</integer>
|
||||
</resources>
|
@@ -1,24 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
All localisable strings are now in the 'libsession' module (which will be renamed to
|
||||
'common' in the near future), and all content description / AccessibilityID_ strings are in
|
||||
their own 'content-descriptions' module.
|
||||
-->
|
||||
|
||||
<!-- TODO: These need to be removed and the text generated by DateUtils.getLocalisedRelativeDayString - but
|
||||
it's going to be a nuisance because that returns a string not a resource ID so just leaving them for now -AL -->
|
||||
<string name="BucketedThreadMedia_Today">Today</string>
|
||||
<string name="BucketedThreadMedia_Yesterday">Yesterday</string>
|
||||
|
||||
<!-- TODO: We'll also need plurals for these strings -->
|
||||
<plurals name="ConversationFragment_delete_selected_messages">
|
||||
<item quantity="one">Delete selected message?</item>
|
||||
<item quantity="other">Delete selected messages?</item>
|
||||
</plurals>
|
||||
<plurals name="ConversationFragment_this_will_permanently_delete_all_n_selected_messages">
|
||||
<item quantity="one">This will permanently delete the selected message.</item>
|
||||
<item quantity="other">This will permanently delete all %1$d selected messages.</item>
|
||||
</plurals>
|
||||
|
||||
</resources>
|
@@ -33,7 +33,7 @@
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.Session.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
|
||||
<item name="android:windowBackground">@drawable/default_dialog_background</item>
|
||||
<item name="android:windowBackground">@drawable/default_dialog_background_inset</item>
|
||||
<item name="android:colorBackground">?backgroundSecondary</item>
|
||||
<item name="android:colorBackgroundFloating">?colorPrimary</item>
|
||||
<item name="backgroundTint">?colorPrimary</item>
|
||||
|
@@ -7,9 +7,6 @@
|
||||
android:title="@string/helpReportABug"
|
||||
android:summary="@string/helpReportABugExportLogsDescription"
|
||||
android:widgetLayout="@layout/export_logs_widget" />
|
||||
|
||||
<!-- Note: Having this as `android:layout` rather than `android:layoutWidget` allows it to fit the screen width -->
|
||||
<Preference android:layout="@layout/preference_widget_progress" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory>
|
||||
|
@@ -203,7 +203,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
@Test
|
||||
fun `local recipient should have input and no blinded recipient`() {
|
||||
whenever(recipient.isLocalNumber).thenReturn(true)
|
||||
assertThat(viewModel.hidesInputBar(), equalTo(false))
|
||||
assertThat(viewModel.shouldHideInputBar(), equalTo(false))
|
||||
assertThat(viewModel.blindedRecipient, nullValue())
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
}
|
||||
whenever(repository.maybeGetBlindedRecipient(recipient)).thenReturn(blinded)
|
||||
assertThat(viewModel.blindedRecipient, notNullValue())
|
||||
assertThat(viewModel.hidesInputBar(), equalTo(true))
|
||||
assertThat(viewModel.shouldHideInputBar(), equalTo(true))
|
||||
}
|
||||
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
package org.thoughtcrime.securesms.l10n;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
//FIXME AC: This test group is outdated.
|
||||
@Ignore("This test group uses outdated instrumentation and needs a migration to modern tools.")
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
public final class LanguageResourcesTest {
|
||||
|
||||
@Test
|
||||
public void language_entries_match_language_values_in_length() {
|
||||
Resources resources = ApplicationProvider.getApplicationContext().getResources();
|
||||
String[] values = resources.getStringArray(R.array.language_values);
|
||||
String[] entries = resources.getStringArray(R.array.language_entries);
|
||||
assertEquals(values.length, entries.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void language_options_matches_available_resources() {
|
||||
Set<String> languageEntries = languageEntries();
|
||||
Set<String> foundResources = buildConfigResources();
|
||||
if (!languageEntries.equals(foundResources)) {
|
||||
assertSubset(foundResources, languageEntries, "Missing language_entries for resources");
|
||||
assertSubset(languageEntries, foundResources, "Missing resources for language_entries");
|
||||
fail("Unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> languageEntries() {
|
||||
Resources resources = ApplicationProvider.getApplicationContext().getResources();
|
||||
String[] values = resources.getStringArray(R.array.language_values);
|
||||
|
||||
List<String> tail = Arrays.asList(values).subList(1, values.length);
|
||||
Set<String> set = new HashSet<>(tail);
|
||||
|
||||
assertEquals("First is not the default", "zz", values[0]);
|
||||
assertEquals("List contains duplicates", tail.size(), set.size());
|
||||
return set;
|
||||
}
|
||||
|
||||
private static Set<String> buildConfigResources() {
|
||||
Set<String> set = new HashSet<>();
|
||||
Collections.addAll(set, BuildConfig.LANGUAGES);
|
||||
assertEquals("List contains duplicates", BuildConfig.LANGUAGES.length, set.size());
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails if "a" is not a subset of "b", lists the additional values found in "a"
|
||||
*/
|
||||
private static void assertSubset(Set<String> a, Set<String> b, String message) {
|
||||
Set<String> delta = subtract(a, b);
|
||||
if (!delta.isEmpty()) {
|
||||
fail(message + ": " + String.join(", ", delta));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a - Set b
|
||||
*/
|
||||
private static Set<String> subtract(Set<String> a, Set<String> b) {
|
||||
Set<String> set = new HashSet<>(a);
|
||||
set.removeAll(b);
|
||||
return set;
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util.dynamiclanguage;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.session.libsession.utilities.dynamiclanguage.LanguageString;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class LanguageStringTest {
|
||||
|
||||
private final Locale expected;
|
||||
private final String input;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
|
||||
/* Language */
|
||||
{ new Locale("en"), "en" },
|
||||
{ new Locale("de"), "de" },
|
||||
{ new Locale("fr"), "FR" },
|
||||
|
||||
/* Language and region */
|
||||
{ new Locale("en", "US"), "en_US" },
|
||||
{ new Locale("es", "US"), "es_US" },
|
||||
{ new Locale("es", "MX"), "es_MX" },
|
||||
{ new Locale("es", "MX"), "es_mx" },
|
||||
{ new Locale("de", "DE"), "de_DE" },
|
||||
|
||||
/* Not parsable input */
|
||||
{ null, null },
|
||||
{ null, "" },
|
||||
{ null, "zz" },
|
||||
{ null, "zz_ZZ" },
|
||||
{ null, "fr_ZZ" },
|
||||
{ null, "zz_FR" },
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public LanguageStringTest(Locale expected, String input) {
|
||||
this.expected = expected;
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse() {
|
||||
assertEquals(expected, LanguageString.parseLocale(input));
|
||||
}
|
||||
}
|
@@ -1,58 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util.dynamiclanguage;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||
|
||||
import network.loki.messenger.BuildConfig;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
//FIXME AC: This test group is outdated.
|
||||
@Ignore("This test group uses outdated instrumentation and needs a migration to modern tools.")
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
public final class LocaleParserTest {
|
||||
|
||||
@Test
|
||||
public void findBestMatchingLocaleForLanguage_all_build_config_languages_can_be_resolved() {
|
||||
for (String lang : buildConfigLanguages()) {
|
||||
Locale locale = LocaleParser.findBestMatchingLocaleForLanguage(lang);
|
||||
assertEquals(lang, locale.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "fr")
|
||||
public void findBestMatchingLocaleForLanguage_a_non_build_config_language_defaults_to_device_value_which_is_supported_directly() {
|
||||
String unsupportedLanguage = getUnsupportedLanguage();
|
||||
assertEquals(Locale.FRENCH, LocaleParser.findBestMatchingLocaleForLanguage(unsupportedLanguage));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "en-rCA")
|
||||
public void findBestMatchingLocaleForLanguage_a_non_build_config_language_defaults_to_device_value_which_is_not_supported_directly() {
|
||||
String unsupportedLanguage = getUnsupportedLanguage();
|
||||
assertEquals(Locale.CANADA, LocaleParser.findBestMatchingLocaleForLanguage(unsupportedLanguage));
|
||||
}
|
||||
|
||||
private static String getUnsupportedLanguage() {
|
||||
String unsupportedLanguage = "af";
|
||||
assertFalse("Language should be an unsupported one", buildConfigLanguages().contains(unsupportedLanguage));
|
||||
return unsupportedLanguage;
|
||||
}
|
||||
|
||||
private static List<String> buildConfigLanguages() {
|
||||
return Arrays.asList(BuildConfig.LANGUAGES);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user