diff --git a/.drone.jsonnet b/.drone.jsonnet index b088e05023..b459742392 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -38,6 +38,7 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/ pull: 'always', environment: { ANDROID_HOME: '/usr/lib/android-sdk' }, commands: [ + 'apt-get update', 'apt-get install -y ninja-build openjdk-17-jdk', 'update-java-alternatives -s java-1.17.0-openjdk-amd64', './gradlew testPlayDebugUnitTestCoverageReport' @@ -79,6 +80,7 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/ pull: 'always', environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, ANDROID_HOME: '/usr/lib/android-sdk' }, commands: [ + 'apt-get update', 'apt-get install -y ninja-build openjdk-17-jdk', 'update-java-alternatives -s java-1.17.0-openjdk-amd64', './gradlew assemblePlayDebug', diff --git a/app/build.gradle b/app/build.gradle index 12d86f303d..6a6c53a5e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,7 +92,6 @@ android { buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "String", "USER_AGENT", "\"OWA\"" - buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" resourceConfigurations += [] diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 1cf7b757bc..274682e837 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -59,8 +59,6 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Toaster; 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; @@ -98,7 +96,6 @@ 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; @@ -116,7 +113,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; @@ -342,10 +338,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO super.onTerminate(); } - public void initializeLocaleParser() { - LocaleParser.Companion.configure(new LocaleParseHelper()); - } - public ExpiringMessageManager getExpiringMessageManager() { return expiringMessageManager; } @@ -443,12 +435,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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java index a99fe83430..c3321504ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java @@ -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))); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java index d44978b05b..8722c0e092 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java @@ -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))); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt index 77b77f863e..3d38857b50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/DeleteMediaDialog.kt @@ -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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MissingMicrophonePermissionDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MissingMicrophonePermissionDialog.kt deleted file mode 100644 index eb7280f628..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/MissingMicrophonePermissionDialog.kt +++ /dev/null @@ -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() - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt index a79ca48888..b3bb4e5b87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt @@ -11,20 +11,16 @@ 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 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 @@ -130,7 +126,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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt index f00fbf44a9..f65dce4974 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/home/StartConversation.kt @@ -66,21 +66,21 @@ internal fun StartConversationScreen( icon = R.drawable.ic_message, modifier = Modifier.contentDescription(R.string.AccessibilityId_messageNew), onClick = delegate::onNewMessageSelected) - Divider(startIndent = LocalDimensions.current.dividerIndent) + Divider(startIndent = LocalDimensions.current.minItemButtonHeight) ItemButton( textId = R.string.groupCreate, icon = R.drawable.ic_group, modifier = Modifier.contentDescription(R.string.AccessibilityId_groupCreate), onClick = delegate::onCreateGroupSelected ) - Divider(startIndent = LocalDimensions.current.dividerIndent) + Divider(startIndent = LocalDimensions.current.minItemButtonHeight) ItemButton( textId = R.string.communityJoin, icon = R.drawable.ic_globe, modifier = Modifier.contentDescription(R.string.AccessibilityId_communityJoin), onClick = delegate::onJoinCommunitySelected ) - Divider(startIndent = LocalDimensions.current.dividerIndent) + Divider(startIndent = LocalDimensions.current.minItemButtonHeight) ItemButton( textId = R.string.sessionInviteAFriend, icon = R.drawable.ic_invite_friend, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt index c6bfa91c51..4d70f40471 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt @@ -98,8 +98,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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 7883828ad8..e7d412a77b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -187,6 +187,7 @@ import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.ui.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme import org.thoughtcrime.securesms.util.ActivityDispatcher +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.NetworkUtils @@ -725,7 +726,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 @@ -898,6 +898,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (!isFinishing) { finish() } + + binding.inputBar.isGone = uiState.hideInputBar } } @@ -997,7 +999,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } binding.declineMessageRequestButton.setOnClickListener { - viewModel.declineMessageRequest() + fun doDecline() { + viewModel.declineMessageRequest() + finish() + } + + showSessionDialog { + title(R.string.delete) + text(resources.getString(R.string.messageRequestsDelete)) + dangerButton(R.string.delete) { doDecline() } + button(R.string.cancel) + } } lifecycleScope.launch { @@ -1018,6 +1030,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } + private fun acceptMessageRequest() { + binding.messageRequestBar.isVisible = false + viewModel.acceptMessageRequest() + } + override fun inputBarEditTextContentChanged(newContent: CharSequence) { val inputBarText = binding.inputBar.text // TODO check if we should be referencing newContent here instead if (textSecurePreferences.isLinkPreviewsEnabled()) { @@ -1826,10 +1843,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) @@ -2135,7 +2163,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 @@ -2173,16 +2201,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) { 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) } } @@ -2501,7 +2529,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // Note: The adapter itemCount is zero based - so calling this with the itemCount in // a non-zero based manner scrolls us to the bottom of the last message (including // to the bottom of long messages as required by Jira SES-789 / GitHub 1364). - recyclerView.scrollToPosition(adapter.itemCount) + recyclerView.smoothScrollToPosition(adapter.itemCount) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 8b468c3606..880dacb070 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 725806e0c0..1905ab306f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -11,8 +11,12 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext 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 network.loki.messenger.R @@ -34,6 +38,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.ThreadDatabase 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 @@ -84,6 +89,8 @@ class ConversationViewModel( return repository.getInvitingAdmin(threadId) } + private var communityWriteAccessJob: Job? = null + private var _openGroup: RetrieveOnce = RetrieveOnce { storage.getOpenGroup(threadId) } @@ -143,6 +150,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()) + } + } + } } /** @@ -396,7 +424,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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt index 416a796ea6..d9e6e22a4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt @@ -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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt index d4489dae3c..143951d2bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt @@ -205,13 +205,17 @@ class MentionViewModel( val sb = StringBuilder() var offset = 0 for ((span, range) in spansWithRanges) { - // Add content before the mention span - sb.append(editable, offset, range.first) + // Add content before the mention span. There's a possibility of overlapping spans so we need to + // safe guard the start offset here to not go over our span's start. + val thisMentionStart = range.first + val lastMentionEnd = offset.coerceAtMost(thisMentionStart) + sb.append(editable, lastMentionEnd, thisMentionStart) // Replace the mention span with "@public key" sb.append('@').append(span.member.publicKey).append(' ') - offset = range.last + 1 + // Safe guard offset to not go over the end of the editable. + offset = (range.last + 1).coerceAtMost(editable.length) } // Add the remaining content diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index 2c07ea03bd..fe0ba1ac76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -36,8 +36,6 @@ import org.session.libsignal.utilities.AccountId 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 @@ -49,11 +47,14 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.groups.EditGroupActivity import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity.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 object ConversationMenuHelper { @@ -211,7 +212,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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt index 666bf9360c..01d5b41f7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -19,11 +19,10 @@ 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.messaging.utilities.UpdateMessageData -import org.session.libsession.utilities.getColorFromAttr +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 @@ -31,6 +30,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 @@ -136,15 +136,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) -> { @@ -171,6 +162,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() + } + } + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java index d042a30196..ccbba13b3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java @@ -16,7 +16,6 @@ */ package org.thoughtcrime.securesms.conversation.v2.utilities; -import static com.google.android.gms.common.util.CollectionUtils.listOf; import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY; import android.Manifest; @@ -254,7 +253,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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt index 83932b2ce4..b7103b9c23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt @@ -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 { - 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 6523b4444e..c7da1ed81d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -1892,6 +1892,7 @@ open class Storage( configFactory.withMutableUserConfigs { it.contacts.upsertContact(recipient.address.serialize()) { this.approved = approved + this.priority = PRIORITY_VISIBLE } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java deleted file mode 100644 index 40b8c36dd2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java +++ /dev/null @@ -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 { - - @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 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 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 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 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> 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 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 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); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 90bba49d28..7a82a34ebb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -104,24 +104,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) { @@ -151,8 +133,13 @@ public class ThreadRecord extends DisplayRecord { .put(NAME_KEY, getName()) .format().toString(); } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) { - // Remove formatting on the message by calling .getString() on the SpannableString - return lastMessage != null ? lastMessage.getDisplayBody(context).toString() : null; + // 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 ""; + } } else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) { return Phrase.from(context, R.string.attachmentsMediaSaved) .put(NAME_KEY, getName()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index 095a505dd1..d3fbac350c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -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 @@ -38,6 +41,9 @@ object OpenGroupManager { return true } + // flow holding information on write access for our current communities + private val _communityWriteAccess: MutableStateFlow> = MutableStateFlow(emptyMap()) + fun startPolling() { if (isPolling) { return } isPolling = true @@ -65,6 +71,8 @@ object OpenGroupManager { } } + fun getCommunitiesWriteAccessFlow() = _communityWriteAccess.asStateFlow() + @WorkerThread fun add(server: String, room: String, publicKey: String, context: Context): Pair { 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt index 617f98a9cd..a413d09d86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/FixedTimeBuckets.kt b/app/src/main/java/org/thoughtcrime/securesms/media/FixedTimeBuckets.kt index 85b00f66bc..ecfab34aab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/FixedTimeBuckets.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/FixedTimeBuckets.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt index 79697252bd..34ccc1c1c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt @@ -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), diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt index 25ea26b197..b856745e47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt index 341831472f..279b06d387 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt @@ -110,11 +110,7 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat showSessionDialog { title(R.string.delete) text(resources.getString(R.string.messageRequestsDelete)) - if (thread.recipient.isClosedGroupV2Recipient) { - dangerButton(R.string.delete, contentDescriptionRes = R.string.delete) { doDecline() } - } else { - dangerButton(R.string.decline, contentDescriptionRes = R.string.decline) { doDecline() } - } + dangerButton(R.string.delete) { doDecline() } button(R.string.cancel) } } @@ -132,9 +128,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) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt index e1b5d3f03c..08e8954dce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt @@ -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() } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt index 7383b708e0..83f0ae3de3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/CallToggleListener.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 0d9ccb3b37..b03857ed95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -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 @@ -511,7 +510,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { ) { startAvatarSelection() } - .testTag(stringResource(R.string.AccessibilityId_avatarPicker)) + .qaTag(stringResource(R.string.AccessibilityId_avatarPicker)) .background( shape = CircleShape, color = LocalColors.current.backgroundBubbleReceived, @@ -574,6 +573,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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index f0149307bf..08bb511bc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -280,23 +280,19 @@ fun ItemButton( onClick: () -> Unit ) { TextButton( - modifier = modifier.fillMaxWidth() - .height(IntrinsicSize.Min) - .heightIn(min = minHeight) - .padding(horizontal = LocalDimensions.current.xsSpacing), + modifier = modifier.fillMaxWidth(), colors = colors, onClick = onClick, + contentPadding = PaddingValues(), shape = RectangleShape, ) { Box( - modifier = Modifier.fillMaxHeight() - .aspectRatio(1f) - .align(Alignment.CenterVertically) - ) { - icon() - } - - Spacer(modifier = Modifier.width(LocalDimensions.current.smallSpacing)) + modifier = Modifier + .padding(horizontal = LocalDimensions.current.xxsSpacing) + .size(minHeight) + .align(Alignment.CenterVertically), + content = icon + ) Text( text, @@ -320,6 +316,18 @@ fun PreviewItemButton() { } } +@Preview +@Composable +fun PreviewLargeItemButton() { + PreviewTheme { + LargeItemButton( + textId = R.string.groupCreate, + icon = R.drawable.ic_group, + onClick = {} + ) + } +} + @Composable fun Cell( modifier: Modifier = Modifier, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Util.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Util.kt index 22eb4bf860..de2271162d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Util.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Util.kt @@ -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.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) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index ea996b59ce..a3b508f481 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -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, @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt index 597302edf6..0152c8b280 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt @@ -122,7 +122,7 @@ fun SessionOutlinedTextField( ) .fillMaxWidth() .wrapContentHeight() - .padding(vertical = 28.dp, horizontal = 21.dp) + .padding(LocalDimensions.current.spacing) ) { innerTextField() diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index ac5ce8c4cf..d1608ea24e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -15,7 +15,6 @@ data class Dimensions( val mediumSpacing: Dp = 36.dp, val xlargeSpacing: Dp = 64.dp, - val dividerIndent: Dp = 60.dp, val appBarHeight: Dp = 64.dp, val minItemButtonHeight: Dp = 50.dp, val minLargeItemButtonHeight: Dp = 60.dp, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt index 10d2fceb79..36c64f7fbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt @@ -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 - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/dynamiclanguage/LocaleParseHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/util/dynamiclanguage/LocaleParseHelper.kt deleted file mode 100644 index 7e0b963aec..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/dynamiclanguage/LocaleParseHelper.kt +++ /dev/null @@ -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 - - } -} \ No newline at end of file diff --git a/app/src/main/res/drawable-ldrtl/ic_arrow_left.xml b/app/src/main/res/drawable-ldrtl/ic_arrow_left.xml new file mode 100644 index 0000000000..fed8ba3b3a --- /dev/null +++ b/app/src/main/res/drawable-ldrtl/ic_arrow_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-ldrtl/ic_arrow_right.xml b/app/src/main/res/drawable-ldrtl/ic_arrow_right.xml new file mode 100644 index 0000000000..aaa8c18c9a --- /dev/null +++ b/app/src/main/res/drawable-ldrtl/ic_arrow_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-ldrtl/search_bar_end.xml b/app/src/main/res/drawable-ldrtl/search_bar_end.xml new file mode 100644 index 0000000000..3abbe7ea96 --- /dev/null +++ b/app/src/main/res/drawable-ldrtl/search_bar_end.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-ldrtl/search_bar_start.xml b/app/src/main/res/drawable-ldrtl/search_bar_start.xml new file mode 100644 index 0000000000..3fb796d88c --- /dev/null +++ b/app/src/main/res/drawable-ldrtl/search_bar_start.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_dialog_background_inset.xml b/app/src/main/res/drawable/default_dialog_background_inset.xml new file mode 100644 index 0000000000..5352b3a3db --- /dev/null +++ b/app/src/main/res/drawable/default_dialog_background_inset.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/image_loading_background.xml b/app/src/main/res/drawable/image_loading_background.xml index d2e3dfbfa7..04c4a8a2a0 100644 --- a/app/src/main/res/drawable/image_loading_background.xml +++ b/app/src/main/res/drawable/image_loading_background.xml @@ -5,6 +5,4 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_input_bar.xml b/app/src/main/res/layout/view_input_bar.xml index ee6bf42411..2b868ece36 100644 --- a/app/src/main/res/layout/view_input_bar.xml +++ b/app/src/main/res/layout/view_input_bar.xml @@ -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" diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8a42dca405..2233037e81 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -30,7 +30,7 @@ 8dp 10dp 64dp - 8dp + 12dp 11dp 8dp 8dp diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index ba1bd68ee0..526ff4a73f 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -7,4 +7,5 @@ 35 35 + 2000 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4071b00f33..78554f9ebb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,24 +1,5 @@ - - - Today - Yesterday - - - - Delete selected message? - Delete selected messages? - - - This will permanently delete the selected message. - This will permanently delete all %1$d selected messages. - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9b926cc1da..3a0a950a6d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -81,7 +81,7 @@