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>
This commit is contained in:
ThomasSession 2024-10-15 10:53:19 +11:00 committed by GitHub
parent 68750e6146
commit f6d50ac858
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 148 additions and 30 deletions

View File

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

View File

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

View File

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

View File

@ -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<MessageRecord>): Commands()
data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands()

View File

@ -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<RecoveryPasswordActivity>() }
) {
hideRecoveryLauncher.launch(Intent(baseContext, RecoveryPasswordActivity::class.java))
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
Divider()
}

View File

@ -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<Boolean>
get() = _showLoader
private val _recoveryHidden: MutableStateFlow<Boolean> = MutableStateFlow(prefs.getHidePassword())
val recoveryHidden: StateFlow<Boolean>
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()

View File

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

View File

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

View File

@ -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"/>
<TextView
android:id="@+id/sendAcceptsTextView"
@ -340,7 +340,7 @@
android:layout_height="@dimen/medium_button_height"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_weight="1"
android:text="@string/decline" />
android:text="@string/delete" />
</LinearLayout>

View File

@ -27,7 +27,7 @@
app:tint="?searchIconColor"
android:contentDescription="@string/search" />
<EditText
android:hint="@string/messages"
android:hint="@string/search"
android:imeOptions="actionSearch"
android:id="@+id/search_input"
android:paddingHorizontal="@dimen/small_spacing"

View File

@ -23,6 +23,12 @@
<item name="android:textSize">@dimen/very_large_font_size</item>
</style>
<style name="MenuTextAppearance" parent="TextAppearance.AppCompat.Widget.ActionBar.Menu">
<item name="android:textAllCaps">false</item>
<item name="android:textSize">@dimen/small2_font_size</item>
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearance.Session.Dialog.Title" parent="TextAppearance.AppCompat.Title">
<item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/medium2_font_size</item>

View File

@ -60,6 +60,8 @@
<item name="conversation_icon_attach_audio">@drawable/ic_audio_dark</item>
<item name="conversation_icon_attach_video">@drawable/ic_video_dark</item>
<item name="android:actionMenuTextAppearance">@style/MenuTextAppearance</item>
</style>
<!-- This should be the default theme for the application. -->

View File

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

View File

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