🎉 Separate messages by identity

Also, allow deleting all messages/conversations in a list
This commit is contained in:
Christian Basler 2018-08-24 17:32:29 +02:00
parent 9f2508c1a5
commit a9602368fb
14 changed files with 218 additions and 71 deletions

View File

@ -85,10 +85,10 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
super.onResume() super.onResume()
initFab(activity as MainActivity) initFab(activity as MainActivity)
updateList() reloadList()
} }
fun updateList() { override fun reloadList() {
adapter.clear() adapter.clear()
context?.let { context -> context?.let { context ->
val addressRepo = Singleton.getAddressRepository(context) val addressRepo = Singleton.getAddressRepository(context)
@ -138,7 +138,7 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
} }
} }
override fun updateList(label: Void) = updateList() override fun updateList(label: Void) = reloadList()
private data class ViewHolder( private data class ViewHolder(
val ctx: Context, val ctx: Context,

View File

@ -33,6 +33,7 @@ import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.Conversation import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.utils.ConversationService import ch.dissem.bitmessage.utils.ConversationService
@ -43,7 +44,9 @@ import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchAct
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import kotlinx.android.synthetic.main.fragment_message_list.* import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.cancelButton
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.alert
import org.jetbrains.anko.support.v4.onUiThread import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
import java.util.* import java.util.*
@ -90,6 +93,7 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
} }
private var emptyTrashMenuItem: MenuItem? = null private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository private lateinit var messageRepo: AndroidMessageRepository
private lateinit var conversationService: ConversationService private lateinit var conversationService: ConversationService
private var activateOnItemClick: Boolean = false private var activateOnItemClick: Boolean = false
@ -103,7 +107,8 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
val conversationIds = messageRepo.findConversations( val conversationIds = messageRepo.findConversations(
currentLabel.value, currentLabel.value,
messageAdapter.itemCount, messageAdapter.itemCount,
PAGE_SIZE PAGE_SIZE,
context?.preferences?.separateIdentities == true
) )
conversationIds.forEach { conversationId -> conversationIds.forEach { conversationId ->
val conversation = conversationService.getConversation(conversationId) val conversation = conversationService.getConversation(conversationId)
@ -139,6 +144,8 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
super.onPause() super.onPause()
} }
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) { private fun doUpdateList(label: Label?) {
val mainActivity = activity as? MainActivity val mainActivity = activity as? MainActivity
swipeableConversationAdapter?.clear(label) swipeableConversationAdapter?.clear(label)
@ -148,6 +155,9 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
return return
} }
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
// I'm not yet sure if it's a good idea in conversation views, so it's off for now
deleteAllMenuItem?.isVisible = false
mainActivity?.apply { mainActivity?.apply {
if ("archive" == label.toString()) { if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive)) updateTitle(getString(R.string.archive))
@ -298,6 +308,7 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu) inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash) emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
} }
@ -307,13 +318,21 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
currentLabel.value?.let { label -> currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true if (label.type != Label.Type.TRASH) return true
doAsync { deleteAllMessages(label)
for (message in messageRepo.findMessages(label)) { }
messageRepo.remove(message) return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
alert(
title = R.string.delete_all_messages_in_list,
message = R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
} }
cancelButton { }
uiThread { doUpdateList(label) } }.show()
}
} }
return true return true
} }
@ -321,6 +340,16 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
} }
} }
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) { override fun updateList(label: Label) {
currentLabel.value = label currentLabel.value = label
} }

View File

@ -20,6 +20,8 @@ package ch.dissem.apps.abit
* @author Christian Basler * @author Christian Basler
*/ */
interface ListHolder<in L> { interface ListHolder<in L> {
fun reloadList()
fun updateList(label: L) fun updateList(label: L)
fun setActivateOnItemClick(activateOnItemClick: Boolean) fun setActivateOnItemClick(activateOnItemClick: Boolean)

View File

@ -29,6 +29,7 @@ import ch.dissem.apps.abit.drawer.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.* import ch.dissem.apps.abit.util.*
@ -92,6 +93,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
private set private set
private lateinit var bmc: BitmessageContext private lateinit var bmc: BitmessageContext
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var accountHeader: AccountHeader private lateinit var accountHeader: AccountHeader
private lateinit var drawer: Drawer private lateinit var drawer: Drawer
@ -104,6 +106,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
instance = WeakReference(this) instance = WeakReference(this)
bmc = Singleton.getBitmessageContext(this) bmc = Singleton.getBitmessageContext(this)
messageRepo = Singleton.getMessageRepository(this)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
fab.hide() fab.hide()
@ -299,8 +302,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
for (label in labels) { for (label in labels) {
addLabelEntry(label) addLabelEntry(label)
} }
currentLabel.value?.let { currentLabel.value?.let { label ->
drawer.setSelection(it.id as Long) drawer.setSelection(label.id as Long)
} }
updateUnread() updateUnread()
} }
@ -334,7 +337,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
when (item.name.textRes) { when (item.name.textRes) {
R.string.contacts_and_subscriptions -> { R.string.contacts_and_subscriptions -> {
if (itemList is AddressListFragment) { if (itemList is AddressListFragment) {
itemList.updateList() itemList.reloadList()
} else { } else {
changeList(AddressListFragment()) changeList(AddressListFragment())
} }
@ -432,7 +435,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
if (item.tag is Label) { if (item.tag is Label) {
val label = item.tag as Label val label = item.tag as Label
if (label !== LABEL_ARCHIVE) { if (label !== LABEL_ARCHIVE) {
val unread = bmc.messages.countUnread(label) val unread = messageRepo.countUnread(label, preferences.separateIdentities)
if (unread > 0) { if (unread > 0) {
(item as PrimaryDrawerItem).withBadge(unread.toString()) (item as PrimaryDrawerItem).withBadge(unread.toString())
} else { } else {

View File

@ -33,6 +33,7 @@ import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
@ -42,9 +43,9 @@ import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchAct
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import kotlinx.android.synthetic.main.fragment_message_list.* import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.doAsync import org.jetbrains.anko.*
import org.jetbrains.anko.support.v4.alert
import org.jetbrains.anko.support.v4.onUiThread import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
import java.util.* import java.util.*
private const val PAGE_SIZE = 15 private const val PAGE_SIZE = 15
@ -89,6 +90,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
} }
private var emptyTrashMenuItem: MenuItem? = null private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository private lateinit var messageRepo: AndroidMessageRepository
private var activateOnItemClick: Boolean = false private var activateOnItemClick: Boolean = false
@ -98,10 +100,12 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
isLoading = true isLoading = true
swipeableMessageAdapter?.let { messageAdapter -> swipeableMessageAdapter?.let { messageAdapter ->
doAsync { doAsync {
val label = currentLabel.value
val messages = messageRepo.findMessages( val messages = messageRepo.findMessages(
currentLabel.value, label,
messageAdapter.itemCount, messageAdapter.itemCount,
PAGE_SIZE PAGE_SIZE,
context?.preferences?.separateIdentities == true && label?.type != Label.Type.BROADCAST
) )
onUiThread { onUiThread {
messageAdapter.addAll(messages) messageAdapter.addAll(messages)
@ -133,6 +137,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
super.onPause() super.onPause()
} }
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) { private fun doUpdateList(label: Label?) {
// If the menu item isn't available yet, we should wait - the method will be called again once it's // If the menu item isn't available yet, we should wait - the method will be called again once it's
// initialized. // initialized.
@ -155,6 +161,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
loadMoreItems() loadMoreItems()
} }
deleteAllMenuItem?.isVisible = label?.type != Label.Type.TRASH
} }
override fun onCreateView( override fun onCreateView(
@ -300,6 +307,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu) inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash) emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
currentLabel.value?.let { doUpdateList(it) } currentLabel.value?.let { doUpdateList(it) }
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
} }
@ -310,13 +318,21 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
currentLabel.value?.let { label -> currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true if (label.type != Label.Type.TRASH) return true
doAsync { deleteAllMessages(label)
for (message in messageRepo.findMessages(label)) { }
messageRepo.remove(message) return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
alert(
title = R.string.delete_all_messages_in_list,
message = R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
} }
cancelButton { }
uiThread { doUpdateList(label) } }.show()
}
} }
return true return true
} }
@ -324,6 +340,16 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
} }
} }
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) { override fun updateList(label: Label) {
currentLabel.value = label currentLabel.value = label
} }

View File

@ -21,8 +21,8 @@ import ch.dissem.bitmessage.entity.BitmessageAddress
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
class ProfileSelectionListener( class ProfileSelectionListener(
private val ctx: Context, private val ctx: Context,
private val fragmentManager: FragmentManager private val fragmentManager: FragmentManager
) : AccountHeader.OnAccountHeaderListener { ) : AccountHeader.OnAccountHeaderListener {
override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean { override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean {
@ -42,6 +42,13 @@ class ProfileSelectionListener(
val tag = profile.tag val tag = profile.tag
if (tag is BitmessageAddress) { if (tag is BitmessageAddress) {
Singleton.setIdentity(tag) Singleton.setIdentity(tag)
MainActivity.apply {
updateUnread()
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
if (itemList is ListHolder<*>) {
itemList.reloadList()
}
}
} }
} }
} }

View File

@ -21,6 +21,7 @@ import android.database.Cursor
import android.database.DatabaseUtils import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.UuidUtils import ch.dissem.apps.abit.util.UuidUtils
import ch.dissem.apps.abit.util.UuidUtils.asUuid import ch.dissem.apps.abit.util.UuidUtils.asUuid
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
@ -38,7 +39,14 @@ import java.util.*
/** /**
* [MessageRepository] implementation using the Android SQL API. * [MessageRepository] implementation using the Android SQL API.
*/ */
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() { class AndroidMessageRepository(private val sql: SqlHelper, private val prefs: Preferences) : AbstractMessageRepository() {
fun findMessages(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean) =
if (label === LABEL_ARCHIVE || label === null) {
find("id NOT IN (SELECT message_id FROM Message_Label)", offset, limit, separateIdentities)
} else {
find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")", offset, limit, separateIdentities)
}
override fun findMessages(label: Label?, offset: Int, limit: Int) = override fun findMessages(label: Label?, offset: Int, limit: Int) =
if (label === LABEL_ARCHIVE) { if (label === LABEL_ARCHIVE) {
@ -54,30 +62,56 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
null null
).toInt() ).toInt()
override fun countUnread(label: Label?) = when { private fun getSelectIdentity(separateIdentities: Boolean): Pair<String, Array<String>> {
label === LABEL_ARCHIVE -> 0 if (separateIdentities) {
label == null -> DatabaseUtils.queryNumEntries( val identity = prefs.currentIdentity
sql.readableDatabase, return if (prefs.separateIdentities && identity != null) {
TABLE_NAME, "AND (type = 'BROADCAST' OR recipient=? OR sender=?)" to arrayOf(identity.address, identity.address)
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))", } else {
arrayOf(Label.Type.UNREAD.name) "" to emptyArray()
).toInt() }
else -> DatabaseUtils.queryNumEntries( } else {
sql.readableDatabase, return "" to emptyArray()
TABLE_NAME, }
" id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(label.id.toString(), Label.Type.UNREAD.name)
).toInt()
} }
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> { override fun countUnread(label: Label?) = countUnread(label, false)
fun countUnread(label: Label?, separateIdentities: Boolean) = getSelectIdentity(separateIdentities).let { (selectIdentityQuery, selectIdentityArgs) ->
when {
label === LABEL_ARCHIVE -> 0
label == null -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
else -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(label.id.toString(), Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
}
}
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> = findConversations(label, offset, limit, false)
fun findConversations(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean): List<UUID> {
val projection = arrayOf(COLUMN_CONVERSATION) val projection = arrayOf(COLUMN_CONVERSATION)
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
val where = when { val where = when {
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)" label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label) $selectIdentityQuery"
label == null -> null label == null -> if (selectIdentityQuery.isNotBlank()) {
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})" "type = 'BROADCAST' OR recipient=? OR sender=?"
} else {
null
}
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id}) $selectIdentityQuery"
} }
val result = LinkedList<UUID>() val result = LinkedList<UUID>()
sql.readableDatabase.query( sql.readableDatabase.query(
@ -85,7 +119,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
TABLE_NAME, TABLE_NAME,
projection, projection,
where, where,
null, null, null, selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC", "$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit" if (limit == 0) null else "$offset, $limit"
).use { c -> ).use { c ->
@ -140,8 +174,11 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
db.update(PARENTS_TABLE_NAME, values, where, null) db.update(PARENTS_TABLE_NAME, values, where, null)
} }
override fun find(where: String, offset: Int, limit: Int): List<Plaintext> { override fun find(where: String, offset: Int, limit: Int) = find(where, offset, limit, false)
fun find(where: String, offset: Int, limit: Int, separateIdentities: Boolean): List<Plaintext> {
val result = LinkedList<Plaintext>() val result = LinkedList<Plaintext>()
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
// Define a projection that specifies which columns from the database // Define a projection that specifies which columns from the database
// you will actually use after this query. // you will actually use after this query.
@ -164,7 +201,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
sql.readableDatabase.query( sql.readableDatabase.query(
TABLE_NAME, projection, TABLE_NAME, projection,
where, null, null, null, "$where $selectIdentityQuery", selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC", "$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit" if (limit == 0) null else "$offset, $limit"
).use { c -> ).use { c ->
@ -318,4 +355,5 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
private const val JT_COLUMN_MESSAGE = "message_id" private const val JT_COLUMN_MESSAGE = "message_id"
private const val JT_COLUMN_LABEL = "label_id" private const val JT_COLUMN_LABEL = "label_id"
} }
} }

View File

@ -64,7 +64,7 @@ object Singleton {
// work-around for messages that are deleted from unread, which already have the unread label removed // work-around for messages that are deleted from unread, which already have the unread label removed
swipeableMessageAdapter.remove(message) swipeableMessageAdapter.remove(message)
} }
label == AndroidLabelRepository.LABEL_ARCHIVE && !added.isEmpty() -> { label == AndroidLabelRepository.LABEL_ARCHIVE && !added.isEmpty() -> {
// work-around for messages in archive, which isn't an actual label but an absence of labels // work-around for messages in archive, which isn't an actual label but an absence of labels
swipeableMessageAdapter.remove(message) swipeableMessageAdapter.remove(message)
} }
@ -110,12 +110,13 @@ object Singleton {
inventory = AndroidInventory(sqlHelper) inventory = AndroidInventory(sqlHelper)
addressRepo = AndroidAddressRepository(sqlHelper) addressRepo = AndroidAddressRepository(sqlHelper)
labelRepo = AndroidLabelRepository(sqlHelper, ctx) labelRepo = AndroidLabelRepository(sqlHelper, ctx)
messageRepo = AndroidMessageRepository(sqlHelper) messageRepo = AndroidMessageRepository(sqlHelper, ctx.preferences)
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it } proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
networkHandler = NioNetworkHandler(4) networkHandler = NioNetworkHandler(4)
listener = getMessageListener(ctx) listener = getMessageListener(ctx)
labeler = Singleton.labeler labeler = Singleton.labeler
preferences.sendPubkeyOnIdentityCreation = false preferences.sendPubkeyOnIdentityCreation = false
preferences.port = context.preferences.listeningPort
} }
} }

View File

@ -29,6 +29,7 @@ object Constants {
const val PREFERENCE_REQUEST_ACK = "request_acknowledgments" const val PREFERENCE_REQUEST_ACK = "request_acknowledgments"
const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms" const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms"
const val PREFERENCE_POW_COUNT = "pow_count" const val PREFERENCE_POW_COUNT = "pow_count"
const val PREFERENCE_SEPARATE_IDENTITIES = "separate_identities"
const val BITMESSAGE_URL_SCHEMA = "bitmessage:" const val BITMESSAGE_URL_SCHEMA = "bitmessage:"

View File

@ -21,10 +21,12 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.BatteryManager import android.os.BatteryManager
import android.os.Build import android.os.Build
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Constants.PREFERENCE_EMULATE_CONVERSATIONS import ch.dissem.apps.abit.util.Constants.PREFERENCE_EMULATE_CONVERSATIONS
import ch.dissem.apps.abit.util.Constants.PREFERENCE_ONLINE import ch.dissem.apps.abit.util.Constants.PREFERENCE_ONLINE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUIRE_CHARGING import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUIRE_CHARGING
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SEPARATE_IDENTITIES
import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY
import org.jetbrains.anko.batteryManager import org.jetbrains.anko.batteryManager
import org.jetbrains.anko.connectivityManager import org.jetbrains.anko.connectivityManager
@ -48,6 +50,8 @@ class Preferences internal constructor(private val ctx: Context) {
private val isAllowedForCharging get() = !requireCharging || isCharging private val isAllowedForCharging get() = !requireCharging || isCharging
private val sharedPreferences = ctx.defaultSharedPreferences
private val isCharging private val isCharging
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ctx.batteryManager.isCharging ctx.batteryManager.isCharging
@ -58,20 +62,20 @@ class Preferences internal constructor(private val ctx: Context) {
} }
var wifiOnly var wifiOnly
get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true) get() = sharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
set(value) { set(value) {
ctx.defaultSharedPreferences.edit() sharedPreferences.edit()
.putBoolean(PREFERENCE_WIFI_ONLY, value) .putBoolean(PREFERENCE_WIFI_ONLY, value)
.apply() .apply()
} }
val requireCharging get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true) val requireCharging get() = sharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
val emulateConversations get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true) val emulateConversations get() = sharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
val exportDirectory by lazy { File(ctx.filesDir, "exports") } val exportDirectory by lazy { File(ctx.filesDir, "exports") }
val requestAcknowledgements = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true) val requestAcknowledgements = sharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
fun cleanupExportDirectory() { fun cleanupExportDirectory() {
if (exportDirectory.exists()) { if (exportDirectory.exists()) {
@ -88,9 +92,9 @@ class Preferences internal constructor(private val ctx: Context) {
} }
var online var online
get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_ONLINE, true) get() = sharedPreferences.getBoolean(PREFERENCE_ONLINE, true)
set(value) { set(value) {
ctx.defaultSharedPreferences.edit() sharedPreferences.edit()
.putBoolean(PREFERENCE_ONLINE, value) .putBoolean(PREFERENCE_ONLINE, value)
.apply() .apply()
if (value) { if (value) {
@ -100,6 +104,16 @@ class Preferences internal constructor(private val ctx: Context) {
} }
} }
val separateIdentities
get() = sharedPreferences.getBoolean(PREFERENCE_SEPARATE_IDENTITIES, false)
val currentIdentity
get() = Singleton.getIdentity(ctx)
val listeningPort
get() = sharedPreferences.getString("listening_port", null)?.toIntOrNull()
?: 8444
companion object { companion object {
private var instance: WeakReference<Preferences>? = null private var instance: WeakReference<Preferences>? = null

View File

@ -1,11 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/empty_trash" android:id="@+id/empty_trash"
app:showAsAction="never" android:icon="@drawable/ic_action_delete"
android:icon="@drawable/ic_action_delete" android:title="@string/empty_trash"
android:title="@string/empty_trash" android:visible="false"
android:visible="false"/> app:showAsAction="never" />
<item
android:id="@+id/delete_all"
android:icon="@drawable/ic_action_delete"
android:title="@string/delete_all_messages_in_list"
android:visible="true"
app:showAsAction="never" />
</menu> </menu>

View File

@ -145,4 +145,5 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu
<string name="preference_group_user_experience">Verhalten</string> <string name="preference_group_user_experience">Verhalten</string>
<string name="preference_group_user_experience_summary">Ändern, wie Nachrichten dargestellt werden</string> <string name="preference_group_user_experience_summary">Ändern, wie Nachrichten dargestellt werden</string>
<string name="bitmessage_service_description">Hält die Verbindung zum Bitmessage-Netzwerk.</string> <string name="bitmessage_service_description">Hält die Verbindung zum Bitmessage-Netzwerk.</string>
<string name="preference_port">Port</string>
</resources> </resources>

View File

@ -156,4 +156,10 @@ As an alternative you could configure a trusted node in the settings, but as of
<string name="online">Online</string> <string name="online">Online</string>
<string name="warning_low_memory">Low memory!</string> <string name="warning_low_memory">Low memory!</string>
<string name="bitmessage_service_description">Keeps the connection to the bitmessage network.</string> <string name="bitmessage_service_description">Keeps the connection to the bitmessage network.</string>
<string name="preference_port">Port</string>
<string name="preference_port_summary">Listen on this port for incoming connections. You might need to shortly go offline before the new port is used.</string>
<string name="preference_separate_identities_summary">Show messages for selected identity only</string>
<string name="preference_separate_identities">Filter messages by identity</string>
<string name="delete_all_messages_in_list">Delete all</string>
<string name="delete_all_messages_in_list_ask">Delete all messages in list?</string>
</resources> </resources>

View File

@ -3,10 +3,15 @@
<PreferenceScreen <PreferenceScreen
android:key="preference_ux" android:key="preference_ux"
android:title="@string/preference_group_user_experience" android:persistent="false"
android:summary="@string/preference_group_user_experience_summary" android:summary="@string/preference_group_user_experience_summary"
android:persistent="false"> android:title="@string/preference_group_user_experience">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="separate_identities"
android:summary="@string/preference_separate_identities_summary"
android:title="@string/preference_separate_identities" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="emulate_conversations" android:key="emulate_conversations"
@ -22,9 +27,9 @@
<PreferenceScreen <PreferenceScreen
android:key="preference_network_and_performance" android:key="preference_network_and_performance"
android:title="@string/preference_group_network_and_performance" android:persistent="false"
android:summary="@string/preference_group_network_and_performance_summary" android:summary="@string/preference_group_network_and_performance_summary"
android:persistent="false"> android:title="@string/preference_group_network_and_performance">
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
@ -46,9 +51,9 @@
<PreferenceScreen <PreferenceScreen
android:key="preference_advanced" android:key="preference_advanced"
android:title="@string/preference_group_advanced" android:persistent="false"
android:summary="@string/preference_group_advanced_summary" android:summary="@string/preference_group_advanced_summary"
android:persistent="false"> android:title="@string/preference_group_advanced">
<Preference <Preference
android:key="cleanup" android:key="cleanup"
@ -68,6 +73,13 @@
android:summary="@string/status_summary" android:summary="@string/status_summary"
android:title="@string/status" /> android:title="@string/status" />
<EditTextPreference
android:defaultValue="8444"
android:key="listening_port"
android:summary="@string/preference_port_summary"
android:title="@string/preference_port"
android:numeric="integer"/>
</PreferenceScreen> </PreferenceScreen>
<Preference <Preference