From f6d50ac85822d49cd087692322d625f139622e31 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 15 Oct 2024 10:53:19 +1100 Subject: [PATCH] Feature/strings nice to haves (#1686) * Initial commit with high level structure for new message deletion logic * Adding admin logic * New dialog styles * Matching existing dialog closer to new designs * Using the theme attribute danger instead of a hardcoded colour * Using classes for the dialogs Also cleaned up older references to align with newer look * Adding cancel handling Cleaning unused code * Handling local deletion with batch message deletion * Reusing the 'delete locally' * Delete on device should "marl the message as deleted", not remove it from the db directly * Displaying "marked as deleted" messages Split the `BASE_DELETED_TYPE` into two types: BASE_DELETED_OUTGOING_TYPE and BASE_DELETED_INCOMING_TYPE so we can differentiate them visually. * Proper handling of merged code * Removed temp bg color * Making sure the deleted message view is visible * Renaming functions for clarity * Adding the ability to customise the text for the deleted control messages * Removing code that was added back from merging dev back in * Using the updated strings * Toast confirmation on 'delete locally' * Recreating xml dialogs in Compose and moved logic in VM * Removing hardcoded strings * Updated message deletion logic Still need to finalise "note to self" and "legacy groups" * Deletion logic rework Moving away from promises * More deletion logic Hndling unsend request retrieval as per figma docs * Making sure multi-select works as expectec * Multi message handling Sharing admin logic * Deleting reactions when deleting a message * Deleting reactions when deleting a message * Grabbing server hash from notification data * Fixed unit tests * Handling deletion od "marked as deleted" messages * Handling Control Messages longpress and deletion * Back up handling of no map data for huawei notifications Also rethemed the send buttona dn home plus button to have better ax contrast by standardising the colour displayed on the accent color to be the same as the one on the sent messages * Removed test line * Reworking the deletion dialogs We removed the 'delete locally' dialog, instead we show the 'delete for everyone' with the second option disabled * Outgoing messages can all be marked as 'delete for everyone' Cleaned up invisible copy button on black bgs * Adding a confirmation dialog when clearing emoji * Message request text update * Restyling menu items to not show in uppercase * Proper hint for seach * Do not show seconds when they're 0 * Making the change to "hidden recovery" reactive so it can be dynamically updated in the settings page. This can be simplified once we make SharedPreferences widely accessible as Flows --------- Co-authored-by: ThomasArtProcessors <71994342+ThomasArtProcessors@users.noreply.github.com> --- app/build.gradle | 6 +-- .../conversation/v2/ConversationActivityV2.kt | 11 ++-- .../conversation/v2/ConversationV2Dialogs.kt | 37 +++++++++++-- .../conversation/v2/ConversationViewModel.kt | 54 +++++++++++++++++++ .../securesms/preferences/SettingsActivity.kt | 24 +++++++-- .../preferences/SettingsViewModel.kt | 11 ++++ .../RecoveryPasswordActivity.kt | 11 +++- .../RecoveryPasswordViewModel.kt | 4 -- .../res/layout/activity_conversation_v2.xml | 4 +- .../res/layout/view_global_search_input.xml | 2 +- app/src/main/res/values/styles.xml | 6 +++ app/src/main/res/values/themes.xml | 2 + .../v2/ConversationViewModelTest.kt | 3 +- .../libsession/utilities/LocalisedTimeUtil.kt | 3 +- 14 files changed, 148 insertions(+), 30 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 95e37a849c..da0d2f3fa5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -268,9 +268,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.paging:paging-runtime-ktx:$pagingVersion" - implementation 'androidx.activity:activity-ktx:1.5.1' - implementation 'androidx.activity:activity-compose:1.5.1' - implementation 'androidx.fragment:fragment-ktx:1.5.3' + implementation 'androidx.activity:activity-ktx:1.9.2' + implementation 'androidx.activity:activity-compose:1.9.2' + implementation 'androidx.fragment:fragment-ktx:1.8.4' implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.work:work-runtime-ktx:2.7.1" 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 ad2db82cfb..0132a876cf 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 @@ -1536,14 +1536,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe sendEmojiRemoval(emoji, message) } + /** + * Called when the user is attempting to clear all instance of a specific emoji. + */ override fun onClearAll(emoji: String, messageId: MessageId) { - reactionDb.deleteEmojiReactions(emoji, messageId) - viewModel.openGroup?.let { openGroup -> - lokiMessageDb.getServerID(messageId.id, !messageId.mms)?.let { serverId -> - OpenGroupApi.deleteAllReactions(openGroup.room, openGroup.server, serverId, emoji) - } - } - threadDb.notifyThreadUpdated(viewModel.threadId) + viewModel.onEmojiClear(emoji, messageId) } override fun onMicrophoneButtonMove(event: MotionEvent) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt index 26775f01c2..b9756a13be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt @@ -14,12 +14,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.squareup.phrase.Phrase import network.loki.messenger.R -import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteAllDevicesDialog -import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteEveryoneDialog -import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedForEveryone -import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedLocally -import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog +import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY +import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.* import org.thoughtcrime.securesms.ui.AlertDialog import org.thoughtcrime.securesms.ui.DialogButtonModel import org.thoughtcrime.securesms.ui.GetString @@ -202,6 +201,34 @@ fun ConversationV2Dialogs( ) } + + // Clear emoji + if(dialogsState.clearAllEmoji != null){ + AlertDialog( + onDismissRequest = { + // hide dialog + sendCommand(HideClearEmoji) + }, + text = stringResource(R.string.emojiReactsClearAll).let { txt -> + Phrase.from(txt).put(EMOJI_KEY, dialogsState.clearAllEmoji.emoji).format().toString() + }, + buttons = listOf( + DialogButtonModel( + text = GetString(stringResource(id = R.string.clear)), + color = LocalColors.current.danger, + onClick = { + // delete emoji + sendCommand( + ClearEmoji(dialogsState.clearAllEmoji.emoji, dialogsState.clearAllEmoji.messageId) + ) + } + ), + DialogButtonModel( + GetString(stringResource(R.string.cancel)) + ) + ) + ) + } } } 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 316aeec86c..29a9b9d79d 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 @@ -37,7 +37,10 @@ import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.ReactionDatabase import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.groups.OpenGroupManager @@ -52,6 +55,8 @@ class ConversationViewModel( private val repository: ConversationRepository, private val storage: Storage, private val messageDataProvider: MessageDataProvider, + private val threadDb: ThreadDatabase, + private val reactionDb: ReactionDatabase, private val lokiMessageDb: LokiMessageDatabase, private val textSecurePreferences: TextSecurePreferences ) : ViewModel() { @@ -720,6 +725,12 @@ class ConversationViewModel( } } + is Commands.HideClearEmoji -> { + _dialogsState.update { + it.copy(clearAllEmoji = null) + } + } + is Commands.HideDeleteAllDevicesDialog -> { _dialogsState.update { it.copy(deleteAllDevices = null) @@ -737,6 +748,35 @@ class ConversationViewModel( is Commands.MarkAsDeletedForEveryone -> { markAsDeletedForEveryone(command.data) } + + + is Commands.ClearEmoji -> { + clearEmoji(command.emoji, command.messageId) + } + } + } + + private fun clearEmoji(emoji: String, messageId: MessageId){ + viewModelScope.launch(Dispatchers.Default) { + reactionDb.deleteEmojiReactions(emoji, messageId) + openGroup?.let { openGroup -> + lokiMessageDb.getServerID(messageId.id, !messageId.mms)?.let { serverId -> + OpenGroupApi.deleteAllReactions( + openGroup.room, + openGroup.server, + serverId, + emoji + ) + } + } + threadDb.notifyThreadUpdated(threadId) + } + } + + fun onEmojiClear(emoji: String, messageId: MessageId) { + // show a confirmation dialog + _dialogsState.update { + it.copy(clearAllEmoji = ClearAllEmoji(emoji, messageId)) } } @@ -753,6 +793,8 @@ class ConversationViewModel( private val repository: ConversationRepository, private val storage: Storage, private val messageDataProvider: MessageDataProvider, + private val threadDb: ThreadDatabase, + private val reactionDb: ReactionDatabase, private val lokiMessageDb: LokiMessageDatabase, private val textSecurePreferences: TextSecurePreferences ) : ViewModelProvider.Factory { @@ -765,6 +807,8 @@ class ConversationViewModel( repository = repository, storage = storage, messageDataProvider = messageDataProvider, + threadDb = threadDb, + reactionDb = reactionDb, lokiMessageDb = lokiMessageDb, textSecurePreferences = textSecurePreferences ) as T @@ -773,6 +817,7 @@ class ConversationViewModel( data class DialogsState( val openLinkDialogUrl: String? = null, + val clearAllEmoji: ClearAllEmoji? = null, val deleteEveryone: DeleteForEveryoneDialogData? = null, val deleteAllDevices: DeleteForEveryoneDialogData? = null, ) @@ -785,10 +830,19 @@ class ConversationViewModel( val warning: String? = null ) + data class ClearAllEmoji( + val emoji: String, + val messageId: MessageId + ) + sealed class Commands { data class ShowOpenUrlDialog(val url: String?) : Commands() + + data class ClearEmoji(val emoji:String, val messageId: MessageId) : Commands() + data object HideDeleteEveryoneDialog : Commands() data object HideDeleteAllDevicesDialog : Commands() + data object HideClearEmoji : Commands() data class MarkAsDeletedLocally(val messages: Set): Commands() data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands() 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 bbf075ca7c..eaebcb57f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -134,6 +134,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { cropImage(inputFile, outputFile) } + private val hideRecoveryLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult + + if(result.data?.getBooleanExtra(RecoveryPasswordActivity.RESULT_RECOVERY_HIDDEN, false) == true){ + viewModel.permanentlyHidePassword() + } + } + private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage) private var showAvatarDialog: Boolean by mutableStateOf(false) @@ -183,7 +193,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } binding.composeView.setThemedContent { - Buttons() + val recoveryHidden by viewModel.recoveryHidden.collectAsState() + Buttons(recoveryHidden = recoveryHidden) } lifecycleScope.launch { @@ -390,7 +401,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } @Composable - fun Buttons() { + fun Buttons( + recoveryHidden: Boolean + ) { Column( modifier = Modifier .padding(horizontal = LocalDimensions.current.spacing) @@ -452,12 +465,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { Divider() // Only show the recovery password option if the user has not chosen to permanently hide it - if (!prefs.getHidePassword()) { + if (!recoveryHidden) { LargeItemButton( R.string.sessionRecoveryPassword, R.drawable.ic_shield_outline, Modifier.contentDescription(R.string.AccessibilityId_sessionRecoveryPasswordMenuItem) - ) { push() } + ) { + hideRecoveryLauncher.launch(Intent(baseContext, RecoveryPasswordActivity::class.java)) + overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) + } Divider() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt index bedc913109..5b6fa78d44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R @@ -65,6 +66,10 @@ class SettingsViewModel @Inject constructor( val showLoader: StateFlow get() = _showLoader + private val _recoveryHidden: MutableStateFlow = MutableStateFlow(prefs.getHidePassword()) + val recoveryHidden: StateFlow + get() = _recoveryHidden + /** * Refreshes the avatar on the main settings page */ @@ -230,6 +235,12 @@ class SettingsViewModel @Inject constructor( } } + fun permanentlyHidePassword() { + //todo we can simplify this once we expose all our sharedPrefs as flows + prefs.setHidePassword(true) + _recoveryHidden.update { true } + } + sealed class AvatarDialogState() { object NoAvatar : AvatarDialogState() data class UserAvatar(val address: Address) : AvatarDialogState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordActivity.kt index a6d38c13a0..cc9630ef57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordActivity.kt @@ -1,16 +1,21 @@ package org.thoughtcrime.securesms.recoverypassword +import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import network.loki.messenger.R import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.ui.setComposeContent + class RecoveryPasswordActivity : BaseActionBarActivity() { + companion object { + const val RESULT_RECOVERY_HIDDEN = "recovery_hidden" + } + private val viewModel: RecoveryPasswordViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -25,7 +30,9 @@ class RecoveryPasswordActivity : BaseActionBarActivity() { mnemonic = mnemonic, seed = seed, confirmHideRecovery = { - viewModel.permanentlyHidePassword() + val returnIntent = Intent() + returnIntent.putExtra(RESULT_RECOVERY_HIDDEN, true) + setResult(RESULT_OK, returnIntent) finish() }, copyMnemonic = viewModel::copyMnemonic diff --git a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordViewModel.kt index 0ad207cd23..b159accf23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPasswordViewModel.kt @@ -34,10 +34,6 @@ class RecoveryPasswordViewModel @Inject constructor( .map { MnemonicCodec { MnemonicUtilities.loadFileContents(application, it) }.encode(it, MnemonicCodec.Language.Configuration.english) } .stateIn(viewModelScope, SharingStarted.Eagerly, "") - fun permanentlyHidePassword() { - prefs.setHidePassword(true) - } - fun copyMnemonic() { prefs.setHasViewedSeed(true) ClipData.newPlainText("Seed", mnemonic.value) diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index 77702dc4eb..dd62a33007 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -304,7 +304,7 @@ android:paddingHorizontal="@dimen/massive_spacing" android:paddingVertical="@dimen/small_spacing" android:textSize="@dimen/text_size" - android:text="@string/block"/> + android:text="@string/deleteAfterGroupPR1BlockUser"/> + android:text="@string/delete" /> diff --git a/app/src/main/res/layout/view_global_search_input.xml b/app/src/main/res/layout/view_global_search_input.xml index 2d5c03ec5f..cd164772bd 100644 --- a/app/src/main/res/layout/view_global_search_input.xml +++ b/app/src/main/res/layout/view_global_search_input.xml @@ -27,7 +27,7 @@ app:tint="?searchIconColor" android:contentDescription="@string/search" /> @dimen/very_large_font_size + + diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt index 3a59cff9ab..a12af5f5c1 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt @@ -36,7 +36,8 @@ class ConversationViewModelTest: BaseViewModelTest() { private lateinit var messageRecord: MessageRecord private val viewModel: ConversationViewModel by lazy { - ConversationViewModel(threadId, edKeyPair, application, repository, storage, mock(), mock(), mock()) + ConversationViewModel(threadId, edKeyPair, application, repository, storage, + mock(), mock(), mock(), mock(), mock()) } @Before diff --git a/libsession/src/main/java/org/session/libsession/utilities/LocalisedTimeUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/LocalisedTimeUtil.kt index 4733e67563..adb082f8ee 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/LocalisedTimeUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/LocalisedTimeUtil.kt @@ -46,7 +46,8 @@ object LocalisedTimeUtil { "${this.inWholeHours}h ${minutesRemaining}m" } else if (this.inWholeMinutes > 0) { val secondsRemaining = this.minus(1.minutes.times(this.inWholeMinutes.toInt())).inWholeSeconds - "${this.inWholeMinutes}m ${secondsRemaining}s" + if(secondsRemaining > 0) "${this.inWholeMinutes}m ${secondsRemaining}s" + else "${this.inWholeMinutes}m" } else { "0m ${this.inWholeSeconds}s" }