Batch contact delete

This commit is contained in:
Matéo Duparc 2021-05-05 20:54:25 +02:00
parent e7e3db60b4
commit 458be75114
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
12 changed files with 146 additions and 30 deletions

View File

@ -1,6 +1,9 @@
package sushi.hardcore.aira package sushi.hardcore.aira
import android.content.* import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.provider.OpenableColumns import android.provider.OpenableColumns
@ -12,6 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.aira.adapters.ChatAdapter import sushi.hardcore.aira.adapters.ChatAdapter
@ -24,7 +28,6 @@ import sushi.hardcore.aira.databinding.DialogInfoBinding
import sushi.hardcore.aira.utils.FileUtils import sushi.hardcore.aira.utils.FileUtils
import sushi.hardcore.aira.utils.StringUtils import sushi.hardcore.aira.utils.StringUtils
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.*
class ChatActivity : AppCompatActivity() { class ChatActivity : AppCompatActivity() {
private external fun generateFingerprint(publicKey: ByteArray): String private external fun generateFingerprint(publicKey: ByteArray): String
@ -185,6 +188,7 @@ class ChatActivity : AppCompatActivity() {
airaService.isAppInBackground = false airaService.isAppInBackground = false
if (airaService.isOnline(sessionId)) { if (airaService.isOnline(sessionId)) {
onConnected() onConnected()
binding.recyclerChat.updatePadding(bottom = 0)
} }
airaService.setSeen(sessionId, true) airaService.setSeen(sessionId, true)
} }

View File

@ -32,9 +32,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var airaService: AIRAService private lateinit var airaService: AIRAService
private lateinit var onlineSessionAdapter: SessionAdapter private lateinit var onlineSessionAdapter: SessionAdapter
private var offlineSessionAdapter: SessionAdapter? = null private var offlineSessionAdapter: SessionAdapter? = null
private val onSessionsItemClick = AdapterView.OnItemClickListener { adapter, _, position, _ ->
launchChatActivity(adapter.getItemAtPosition(position) as Session)
}
private val onSessionsItemClickSendFile = AdapterView.OnItemClickListener { adapter, _, position, _ -> private val onSessionsItemClickSendFile = AdapterView.OnItemClickListener { adapter, _, position, _ ->
askShareFileTo(adapter.getItemAtPosition(position) as Session) askShareFileTo(adapter.getItemAtPosition(position) as Session)
} }
@ -97,8 +94,18 @@ class MainActivity : AppCompatActivity() {
onItemClickListener = if (openedToShareFile) { onItemClickListener = if (openedToShareFile) {
onSessionsItemClickSendFile onSessionsItemClickSendFile
} else { } else {
onSessionsItemClick AdapterView.OnItemClickListener { _, _, position, _ ->
if (isSelecting()) {
changeSelection(onlineSessionAdapter, position)
} else {
launchChatActivity(onlineSessionAdapter.getItem(position))
}
}
} }
onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, position, _ ->
changeSelection(onlineSessionAdapter, position)
true
}
setOnScrollListener(onSessionsScrollListener) setOnScrollListener(onSessionsScrollListener)
} }
if (openedToShareFile) { if (openedToShareFile) {
@ -111,7 +118,17 @@ class MainActivity : AppCompatActivity() {
onItemClickListener = if (openedToShareFile) { onItemClickListener = if (openedToShareFile) {
onSessionsItemClickSendFile onSessionsItemClickSendFile
} else { } else {
onSessionsItemClick AdapterView.OnItemClickListener { _, _, position, _ ->
if (isSelecting()) {
changeSelection(offlineSessionAdapter!!, position)
} else {
launchChatActivity(offlineSessionAdapter!!.getItem(position))
}
}
}
onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, position, _ ->
changeSelection(offlineSessionAdapter!!, position)
true
} }
setOnScrollListener(onSessionsScrollListener) setOnScrollListener(onSessionsScrollListener)
} }
@ -166,8 +183,9 @@ class MainActivity : AppCompatActivity() {
} }
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_activity, menu) menuInflater.inflate(R.menu.main_activity, menu)
menu.findItem(R.id.remove_contact).isVisible = isSelecting()
return true return true
} }
@ -194,6 +212,30 @@ class MainActivity : AppCompatActivity() {
} }
true true
} }
R.id.remove_contact -> {
AlertDialog.Builder(this)
.setTitle(R.string.warning)
.setMessage(R.string.ask_remove_contacts)
.setPositiveButton(R.string.delete) { _, _ ->
Thread {
for (sessionId in onlineSessionAdapter.getSelectedSessionIds()) {
airaService.removeContact(sessionId)
}
offlineSessionAdapter?.let {
for (sessionId in it.getSelectedSessionIds()) {
airaService.removeContact(sessionId)
}
}
runOnUiThread {
unSelectAll()
refreshSessions()
}
}.start()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@ -211,10 +253,7 @@ class MainActivity : AppCompatActivity() {
if (AIRAService.isServiceRunning) { if (AIRAService.isServiceRunning) {
airaService.isAppInBackground = false airaService.isAppInBackground = false
airaService.uiCallbacks = uiCallbacks //restoring callbacks airaService.uiCallbacks = uiCallbacks //restoring callbacks
onlineSessionAdapter.reset() refreshSessions()
offlineSessionAdapter?.reset()
loadContacts()
loadSessions()
title = airaService.identityName title = airaService.identityName
} else { } else {
finish() finish()
@ -222,6 +261,40 @@ class MainActivity : AppCompatActivity() {
} }
} }
override fun onBackPressed() {
if (isSelecting()) {
unSelectAll()
} else {
super.onBackPressed()
}
}
private fun refreshSessions() {
onlineSessionAdapter.reset()
offlineSessionAdapter?.reset()
loadContacts()
loadSessions()
}
private fun unSelectAll() {
onlineSessionAdapter.unSelectAll()
offlineSessionAdapter?.unSelectAll()
invalidateOptionsMenu()
}
private fun changeSelection(adapter: SessionAdapter, position: Int) {
val wasSelecting = adapter.selectedItems.isNotEmpty()
adapter.onSelectionChanged(position)
val isSelecting = adapter.selectedItems.isNotEmpty()
if (wasSelecting != isSelecting) {
invalidateOptionsMenu()
}
}
private fun isSelecting(): Boolean {
return onlineSessionAdapter.selectedItems.isNotEmpty() || offlineSessionAdapter?.selectedItems?.isNotEmpty() == true
}
private fun loadContacts() { private fun loadContacts() {
if (offlineSessionAdapter != null) { if (offlineSessionAdapter != null) {
for ((sessionId, contact) in airaService.contacts) { for ((sessionId, contact) in airaService.contacts) {

View File

@ -11,6 +11,8 @@ import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.marginEnd
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.aira.ChatItem import sushi.hardcore.aira.ChatItem
import sushi.hardcore.aira.R import sushi.hardcore.aira.R
@ -44,9 +46,11 @@ class ChatAdapter(
internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) { internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) {
fun handleItemView(position: Int) { fun handleItemView(position: Int) {
if (position == 0) { itemView.updatePadding(top = if (position == 0) {
itemView.setPadding(itemView.paddingLeft, 50, itemView.paddingRight, itemView.paddingBottom) 50
} } else {
itemView.paddingBottom
})
} }
fun setBubbleColor(bubble: View, outgoing: Boolean) { fun setBubbleColor(bubble: View, outgoing: Boolean) {
if (outgoing) { if (outgoing) {

View File

@ -8,12 +8,14 @@ import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import sushi.hardcore.aira.R import sushi.hardcore.aira.R
import sushi.hardcore.aira.widgets.TextAvatar import sushi.hardcore.aira.widgets.TextAvatar
class SessionAdapter(context: Context): BaseAdapter() { class SessionAdapter(val context: Context): BaseAdapter() {
private val sessions = mutableListOf<Session>() private val sessions = mutableListOf<Session>()
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
val selectedItems = mutableListOf<Int>()
override fun getCount(): Int { override fun getCount(): Int {
return sessions.size return sessions.size
@ -55,6 +57,11 @@ class SessionAdapter(context: Context): BaseAdapter() {
} else { } else {
View.VISIBLE View.VISIBLE
} }
view.setBackgroundColor(ContextCompat.getColor(context, if (selectedItems.contains(position)) {
R.color.itemSelected
} else {
R.color.sessionBackground
}))
return view return view
} }
@ -99,4 +106,22 @@ class SessionAdapter(context: Context): BaseAdapter() {
sessions.clear() sessions.clear()
notifyDataSetChanged() notifyDataSetChanged()
} }
fun onSelectionChanged(position: Int) {
if (!selectedItems.contains(position)) {
selectedItems.add(position)
} else {
selectedItems.remove(position)
}
notifyDataSetChanged()
}
fun unSelectAll() {
selectedItems.clear()
notifyDataSetChanged()
}
fun getSelectedSessionIds(): List<Int> {
return selectedItems.map { position -> sessions[position].sessionId }
}
} }

View File

@ -14,6 +14,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.RemoteInput import androidx.core.app.RemoteInput
import sushi.hardcore.aira.* import sushi.hardcore.aira.*
import sushi.hardcore.aira.utils.StringUtils
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.net.* import java.net.*
@ -556,7 +557,7 @@ class AIRAService : Service() {
} }
} }
Protocol.TELL_NAME -> { Protocol.TELL_NAME -> {
val name = buffer.sliceArray(1 until buffer.size).decodeToString() val name = StringUtils.sanitizeName(buffer.sliceArray(1 until buffer.size).decodeToString())
uiCallbacks?.onNameTold(sessionId, name) uiCallbacks?.onNameTold(sessionId, name)
val contact = contacts[sessionId] val contact = contacts[sessionId]
if (contact == null) { if (contact == null) {

View File

@ -21,4 +21,8 @@ object StringUtils {
rawIp.substring(0, i) rawIp.substring(0, i)
} }
} }
fun sanitizeName(name: String): String {
return name.replace('\n', ' ')
}
} }

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/sessionBackground"/>
</shape>
</item>
</selector>

View File

@ -10,7 +10,8 @@
android:id="@+id/recycler_chat" android:id="@+id/recycler_chat"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginHorizontal="20dp" android:paddingHorizontal="20dp"
android:paddingBottom="20dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/> app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>

View File

@ -4,8 +4,7 @@
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="10dp" android:padding="10dp">
android:background="@drawable/background_session">
<sushi.hardcore.aira.widgets.TextAvatar <sushi.hardcore.aira.widgets.TextAvatar
android:id="@+id/text_avatar" android:id="@+id/text_avatar"

View File

@ -5,11 +5,20 @@
<item <item
android:id="@+id/settings" android:id="@+id/settings"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:icon="@drawable/ic_settings"/> android:icon="@drawable/ic_settings"
android:title="@string/settings" />
<item <item
android:id="@+id/close" android:id="@+id/close"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:icon="@drawable/ic_close"/> android:icon="@drawable/ic_close"
android:title="@string/log_out"/>
<item
android:id="@+id/remove_contact"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_person_remove"
android:title="@string/remove_contact"
android:visible="false"/>
</menu> </menu>

View File

@ -9,4 +9,5 @@
<color name="incomingBubbleBackground">@color/secondary</color> <color name="incomingBubbleBackground">@color/secondary</color>
<color name="textLink">#3845A3</color> <color name="textLink">#3845A3</color>
<color name="messageTextColor">#ffffff</color> <color name="messageTextColor">#ffffff</color>
<color name="itemSelected">#66666666</color>
</resources> </resources>

View File

@ -42,6 +42,7 @@
<string name="ask_delete_conversation">Deleting a conversation only affects you. Your contact will still have a copy of this conversation if she/he doesn\'t delete it too. Do you really want to delete all this conversation (messages and files) ?</string> <string name="ask_delete_conversation">Deleting a conversation only affects you. Your contact will still have a copy of this conversation if she/he doesn\'t delete it too. Do you really want to delete all this conversation (messages and files) ?</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="ask_remove_contact">Deleting contact will remove her/his identity key and your conversation (messages and files). You won\'t be able to recognize her/him anymore. This action only affects you. Do you really want to remove this contact ?</string> <string name="ask_remove_contact">Deleting contact will remove her/his identity key and your conversation (messages and files). You won\'t be able to recognize her/him anymore. This action only affects you. Do you really want to remove this contact ?</string>
<string name="ask_remove_contacts">Deleting contacts will remove their identity keys and your conversations (messages and files). You won\'t be able to recognize them anymore. This action only affects you. Do you really want to remove these contacts ?</string>
<string name="enable_password">Encrypt with a password</string> <string name="enable_password">Encrypt with a password</string>
<string name="msg_notification_channel_name">New Messages</string> <string name="msg_notification_channel_name">New Messages</string>
<string name="mark_read">Mark read</string> <string name="mark_read">Mark read</string>
@ -72,4 +73,6 @@
<string name="details">Details</string> <string name="details">Details</string>
<string name="your_addresses">Your IP addresses:</string> <string name="your_addresses">Your IP addresses:</string>
<string name="file_transfer_already_in_progress">Another file transfer is already in progress</string> <string name="file_transfer_already_in_progress">Another file transfer is already in progress</string>
<string name="settings">Settings</string>
<string name="log_out">Log out</string>
</resources> </resources>