Show messages in inbox and archive as conversations
Work in progress - detail view not yet adapted, and needs extended encoding for sensible results.
This commit is contained in:
parent
40f8bc87a2
commit
101913a531
@ -0,0 +1,341 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Christian Basler
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.dissem.apps.abit
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.Fragment
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.support.v7.widget.RecyclerView.OnScrollListener
|
||||||
|
import android.view.*
|
||||||
|
import android.widget.Toast
|
||||||
|
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||||
|
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||||
|
import ch.dissem.apps.abit.adapter.SwipeableConversationAdapter
|
||||||
|
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||||
|
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||||
|
import ch.dissem.apps.abit.service.Singleton
|
||||||
|
import ch.dissem.apps.abit.service.Singleton.currentLabel
|
||||||
|
import ch.dissem.apps.abit.util.FabUtils
|
||||||
|
import ch.dissem.bitmessage.entity.Conversation
|
||||||
|
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||||
|
import ch.dissem.bitmessage.utils.ConversationService
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
|
||||||
|
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||||
|
import kotlinx.android.synthetic.main.fragment_message_list.*
|
||||||
|
import org.jetbrains.anko.doAsync
|
||||||
|
import org.jetbrains.anko.support.v4.onUiThread
|
||||||
|
import org.jetbrains.anko.uiThread
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 15
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list fragment representing a list of Messages. This fragment
|
||||||
|
* also supports tablet devices by allowing list items to be given an
|
||||||
|
* 'activated' state upon selection. This helps indicate which item is
|
||||||
|
* currently being viewed in a [MessageDetailFragment].
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||||
|
* interface.
|
||||||
|
*/
|
||||||
|
class ConversationListFragment : Fragment(), ListHolder<Label> {
|
||||||
|
|
||||||
|
private var isLoading = false
|
||||||
|
private var isLastPage = false
|
||||||
|
|
||||||
|
private var layoutManager: LinearLayoutManager? = null
|
||||||
|
private var swipeableConversationAdapter: SwipeableConversationAdapter? = null
|
||||||
|
private var wrappedAdapter: RecyclerView.Adapter<*>? = null
|
||||||
|
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
|
||||||
|
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null
|
||||||
|
|
||||||
|
private val recyclerViewOnScrollListener = object : OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
||||||
|
layoutManager?.let { layoutManager ->
|
||||||
|
val visibleItemCount = layoutManager.childCount
|
||||||
|
val totalItemCount = layoutManager.itemCount
|
||||||
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
|
|
||||||
|
if (!isLoading && !isLastPage) {
|
||||||
|
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
||||||
|
&& firstVisibleItemPosition >= 0) {
|
||||||
|
loadMoreItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyTrashMenuItem: MenuItem? = null
|
||||||
|
private lateinit var messageRepo: AndroidMessageRepository
|
||||||
|
private lateinit var conversationService: ConversationService
|
||||||
|
private var activateOnItemClick: Boolean = false
|
||||||
|
|
||||||
|
private val backStack = Stack<Label>()
|
||||||
|
|
||||||
|
fun loadMoreItems() {
|
||||||
|
isLoading = true
|
||||||
|
swipeableConversationAdapter?.let { messageAdapter ->
|
||||||
|
doAsync {
|
||||||
|
val conversationIds = messageRepo.findConversations(
|
||||||
|
currentLabel.value,
|
||||||
|
messageAdapter.itemCount,
|
||||||
|
PAGE_SIZE
|
||||||
|
)
|
||||||
|
conversationIds.forEach { conversationId ->
|
||||||
|
val conversation = conversationService.getConversation(conversationId)
|
||||||
|
onUiThread {
|
||||||
|
messageAdapter.add(conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
isLastPage = conversationIds.size < PAGE_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val activity = activity as MainActivity
|
||||||
|
initFab(activity)
|
||||||
|
messageRepo = Singleton.getMessageRepository(activity)
|
||||||
|
conversationService = Singleton.getConversationService(activity)
|
||||||
|
|
||||||
|
currentLabel.addObserver(this) { new -> doUpdateList(new) }
|
||||||
|
doUpdateList(currentLabel.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
currentLabel.removeObserver(this)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doUpdateList(label: Label?) {
|
||||||
|
val mainActivity = activity as? MainActivity
|
||||||
|
swipeableConversationAdapter?.clear(label)
|
||||||
|
if (label == null) {
|
||||||
|
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||||
|
swipeableConversationAdapter?.notifyDataSetChanged()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
|
||||||
|
mainActivity?.apply {
|
||||||
|
if ("archive" == label.toString()) {
|
||||||
|
updateTitle(getString(R.string.archive))
|
||||||
|
} else {
|
||||||
|
updateTitle(label.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View =
|
||||||
|
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val context = context ?: throw IllegalStateException("No context available")
|
||||||
|
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
|
||||||
|
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
|
||||||
|
// animation is running)
|
||||||
|
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
|
||||||
|
setInterceptVerticalScrollingWhileAnimationRunning(true)
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// swipe manager
|
||||||
|
val swipeManager = RecyclerViewSwipeManager()
|
||||||
|
|
||||||
|
//swipeableConversationAdapter
|
||||||
|
val adapter = SwipeableConversationAdapter().apply {
|
||||||
|
setActivateOnItemClick(activateOnItemClick)
|
||||||
|
}
|
||||||
|
adapter.eventListener = object : SwipeableConversationAdapter.EventListener {
|
||||||
|
override fun onItemDeleted(item: Conversation) {
|
||||||
|
item.messages.forEach {
|
||||||
|
Singleton.labeler.delete(it)
|
||||||
|
messageRepo.save(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemArchived(item: Conversation) {
|
||||||
|
item.messages.forEach { Singleton.labeler.archive(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemViewClicked(v: View?) {
|
||||||
|
val position = recycler_view.getChildAdapterPosition(v)
|
||||||
|
adapter.setSelectedPosition(position)
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
adapter.getItem(position).messages.firstOrNull()?.let {
|
||||||
|
MainActivity.apply { onItemSelected(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap for swiping
|
||||||
|
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
|
||||||
|
|
||||||
|
val animator = SwipeDismissItemAnimator()
|
||||||
|
|
||||||
|
// Change animations are enabled by default since support-v7-recyclerview v22.
|
||||||
|
// Disable the change animation in order to make turning back animation of swiped item
|
||||||
|
// works properly.
|
||||||
|
animator.supportsChangeAnimations = false
|
||||||
|
|
||||||
|
recycler_view.layoutManager = layoutManager
|
||||||
|
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableConversationAdapter
|
||||||
|
recycler_view.itemAnimator = animator
|
||||||
|
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||||
|
|
||||||
|
recycler_view.addItemDecoration(
|
||||||
|
SimpleListDividerDecorator(
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
|
// The initialization order is very important! This order determines the priority of
|
||||||
|
// touch event handling.
|
||||||
|
//
|
||||||
|
// priority: TouchActionGuard > Swipe > DragAndDrop
|
||||||
|
touchActionGuardManager.attachRecyclerView(recycler_view)
|
||||||
|
swipeManager.attachRecyclerView(recycler_view)
|
||||||
|
|
||||||
|
recyclerViewTouchActionGuardManager = touchActionGuardManager
|
||||||
|
recyclerViewSwipeManager = swipeManager
|
||||||
|
swipeableConversationAdapter = adapter
|
||||||
|
|
||||||
|
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initFab(context: MainActivity) {
|
||||||
|
val menu = FabSpeedDialMenu(context)
|
||||||
|
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||||
|
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||||
|
FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu)
|
||||||
|
.addOnMenuItemClickListener { _, _, itemId ->
|
||||||
|
val identity = Singleton.getIdentity(context)
|
||||||
|
if (identity == null) {
|
||||||
|
Toast.makeText(
|
||||||
|
activity, R.string.no_identity_warning,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
when (itemId) {
|
||||||
|
1 -> {
|
||||||
|
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||||
|
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||||
|
intent.putExtra(EXTRA_BROADCAST, true)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||||
|
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
recyclerViewSwipeManager?.release()
|
||||||
|
recyclerViewSwipeManager = null
|
||||||
|
|
||||||
|
recyclerViewTouchActionGuardManager?.release()
|
||||||
|
recyclerViewTouchActionGuardManager = null
|
||||||
|
|
||||||
|
recycler_view.itemAnimator = null
|
||||||
|
recycler_view.adapter = null
|
||||||
|
|
||||||
|
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
|
||||||
|
wrappedAdapter = null
|
||||||
|
|
||||||
|
swipeableConversationAdapter = null
|
||||||
|
layoutManager = null
|
||||||
|
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.message_list, menu)
|
||||||
|
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.empty_trash -> {
|
||||||
|
currentLabel.value?.let { label ->
|
||||||
|
if (label.type != Label.Type.TRASH) return true
|
||||||
|
|
||||||
|
doAsync {
|
||||||
|
for (message in messageRepo.findMessages(label)) {
|
||||||
|
messageRepo.remove(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
uiThread { doUpdateList(label) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateList(label: Label) {
|
||||||
|
currentLabel.value = label
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||||
|
swipeableConversationAdapter?.setActivateOnItemClick(activateOnItemClick)
|
||||||
|
this.activateOnItemClick = activateOnItemClick
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
currentLabel.value = backStack.pop()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
@ -18,9 +18,11 @@ package ch.dissem.apps.abit
|
|||||||
|
|
||||||
import android.graphics.*
|
import android.graphics.*
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.support.annotation.ColorInt
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
|
|
||||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||||
|
import org.jetbrains.anko.collections.forEachWithIndex
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Christian Basler
|
* @author Christian Basler
|
||||||
@ -45,8 +47,20 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val color = Color.HSVToColor(floatArrayOf((Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(), 0.8f, 1.0f))
|
private val color = Color.HSVToColor(
|
||||||
private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f))
|
floatArrayOf(
|
||||||
|
(Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(),
|
||||||
|
0.8f,
|
||||||
|
1.0f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
private val background = Color.HSVToColor(
|
||||||
|
floatArrayOf(
|
||||||
|
(Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(),
|
||||||
|
0.8f,
|
||||||
|
1.0f
|
||||||
|
)
|
||||||
|
)
|
||||||
private val textPaint = TextPaint().apply {
|
private val textPaint = TextPaint().apply {
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
color = 0xFF607D8B.toInt()
|
color = 0xFF607D8B.toInt()
|
||||||
@ -54,30 +68,34 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun draw(canvas: Canvas) {
|
override fun draw(canvas: Canvas) {
|
||||||
var x: Float
|
|
||||||
var y: Float
|
|
||||||
val width = canvas.width.toFloat()
|
val width = canvas.width.toFloat()
|
||||||
val height = canvas.height.toFloat()
|
val height = canvas.height.toFloat()
|
||||||
|
draw(canvas, 0f, 0f, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun draw(canvas: Canvas, offsetX: Float, offsetY: Float, width: Float, height: Float) {
|
||||||
|
var x: Float
|
||||||
|
var y: Float
|
||||||
val cellWidth = width / SIZE.toFloat()
|
val cellWidth = width / SIZE.toFloat()
|
||||||
val cellHeight = height / SIZE.toFloat()
|
val cellHeight = height / SIZE.toFloat()
|
||||||
paint.color = background
|
paint.color = background
|
||||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
canvas.drawCircle(offsetX + width / 2, offsetY + height / 2, width / 2, paint)
|
||||||
paint.color = color
|
paint.color = color
|
||||||
for (row in 0 until SIZE) {
|
for (row in 0 until SIZE) {
|
||||||
for (column in 0 until SIZE) {
|
for (column in 0 until SIZE) {
|
||||||
if (fields[row][column]) {
|
if (fields[row][column]) {
|
||||||
x = cellWidth * column
|
x = offsetX + cellWidth * column
|
||||||
y = cellHeight * row
|
y = offsetY + cellHeight * row
|
||||||
canvas.drawCircle(
|
canvas.drawCircle(
|
||||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
||||||
paint
|
paint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isChan) {
|
if (isChan) {
|
||||||
textPaint.textSize = 2 * cellHeight
|
textPaint.textSize = 2 * cellHeight
|
||||||
canvas.drawText("[isChan]", width / 2, 6.7f * cellHeight, textPaint)
|
canvas.drawText("[isChan]", offsetX + width / 2, offsetY + 6.7f * cellHeight, textPaint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,3 +114,67 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
|||||||
private const val CENTER_COLUMN = 5
|
private const val CENTER_COLUMN = 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MultiIdenticon(input: List<BitmessageAddress>, @ColorInt val backgroundColor: Int = Color.WHITE) :
|
||||||
|
Drawable() {
|
||||||
|
|
||||||
|
private val paint = Paint().apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
isAntiAlias = true
|
||||||
|
color = backgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
val identicons = input.map { Identicon(it) }.take(4)
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
val width = canvas.width.toFloat()
|
||||||
|
val height = canvas.height.toFloat()
|
||||||
|
|
||||||
|
when (identicons.size) {
|
||||||
|
0 -> canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||||
|
1 -> identicons.first().draw(canvas)
|
||||||
|
2 -> {
|
||||||
|
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||||
|
val w = width / 2
|
||||||
|
val h = height / 2
|
||||||
|
var x = 0f
|
||||||
|
val y = height / 4
|
||||||
|
identicons.forEach {
|
||||||
|
it.draw(canvas, x, y, w, h)
|
||||||
|
x += w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
val scale = 1f / (1f + 2f * sqrt(3f))
|
||||||
|
val w = width * scale
|
||||||
|
val h = height * scale
|
||||||
|
|
||||||
|
identicons[0].draw(canvas, (width - w) / 2, 0f, w, h)
|
||||||
|
identicons[1].draw(canvas, (width - 2 * w) / 2, h * sqrt(3f) / 2, w, h)
|
||||||
|
identicons[2].draw(canvas, width / 2, h * sqrt(3f) / 2, w, h)
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||||
|
val scale = 1f / (1f + sqrt(2f))
|
||||||
|
val borderScale = 0.5f - scale
|
||||||
|
val w = width * scale
|
||||||
|
val h = height * scale
|
||||||
|
val x = width * borderScale
|
||||||
|
val y = height * borderScale
|
||||||
|
identicons.forEachWithIndex { i, identicon ->
|
||||||
|
identicon.draw(canvas, x + (i % 2) * w, y + (i / 2) * h, w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) {
|
||||||
|
identicons.forEach { it.alpha = alpha }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||||
|
identicons.forEach { it.colorFilter = colorFilter }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||||
|
}
|
||||||
|
@ -324,8 +324,14 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
|||||||
val tag = item.tag
|
val tag = item.tag
|
||||||
if (tag is Label) {
|
if (tag is Label) {
|
||||||
currentLabel.value = tag
|
currentLabel.value = tag
|
||||||
if (itemList !is MessageListFragment) {
|
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
|
||||||
changeList(MessageListFragment())
|
if (itemList !is ConversationListFragment) {
|
||||||
|
changeList(ConversationListFragment())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (itemList !is MessageListFragment) {
|
||||||
|
changeList(MessageListFragment())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
} else if (item is Nameable<*>) {
|
} else if (item is Nameable<*>) {
|
||||||
|
@ -193,7 +193,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
|||||||
adapter.setSelectedPosition(position)
|
adapter.setSelectedPosition(position)
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
val item = adapter.getItem(position)
|
val item = adapter.getItem(position)
|
||||||
(activity as MainActivity).onItemSelected(item)
|
MainActivity.apply { onItemSelected(item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +226,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
|||||||
|
|
||||||
recyclerViewTouchActionGuardManager = touchActionGuardManager
|
recyclerViewTouchActionGuardManager = touchActionGuardManager
|
||||||
recyclerViewSwipeManager = swipeManager
|
recyclerViewSwipeManager = swipeManager
|
||||||
this.swipeableMessageAdapter = adapter
|
swipeableMessageAdapter = adapter
|
||||||
|
|
||||||
Singleton.updateMessageListAdapterInListener(adapter)
|
Singleton.updateMessageListAdapterInListener(adapter)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 Haruki Hasegawa
|
||||||
|
* Copyright 2016 Christian Basler
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.dissem.apps.abit.adapter
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import ch.dissem.apps.abit.MultiIdenticon
|
||||||
|
import ch.dissem.apps.abit.R
|
||||||
|
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
|
||||||
|
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
|
||||||
|
import ch.dissem.bitmessage.entity.Conversation
|
||||||
|
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder
|
||||||
|
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
||||||
|
*
|
||||||
|
* @author Christian Basler
|
||||||
|
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
||||||
|
*/
|
||||||
|
class SwipeableConversationAdapter :
|
||||||
|
RecyclerView.Adapter<SwipeableConversationAdapter.ViewHolder>(),
|
||||||
|
SwipeableItemAdapter<SwipeableConversationAdapter.ViewHolder>, SwipeableItemConstants {
|
||||||
|
|
||||||
|
private val data = LinkedList<Conversation>()
|
||||||
|
var eventListener: EventListener? = null
|
||||||
|
private val itemViewOnClickListener: View.OnClickListener
|
||||||
|
private val swipeableViewContainerOnClickListener: View.OnClickListener
|
||||||
|
|
||||||
|
private var label: Label? = null
|
||||||
|
private var selectedPosition = -1
|
||||||
|
private var activateOnItemClick: Boolean = false
|
||||||
|
|
||||||
|
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||||
|
this.activateOnItemClick = activateOnItemClick
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventListener {
|
||||||
|
fun onItemDeleted(item: Conversation)
|
||||||
|
|
||||||
|
fun onItemArchived(item: Conversation)
|
||||||
|
|
||||||
|
fun onItemViewClicked(v: View?)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
|
||||||
|
val container = v.findViewById<FrameLayout>(R.id.container)!!
|
||||||
|
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
|
||||||
|
val status = v.findViewById<ImageView>(R.id.status)!!
|
||||||
|
val sender = v.findViewById<TextView>(R.id.sender)!!
|
||||||
|
val subject = v.findViewById<TextView>(R.id.subject)!!
|
||||||
|
val extract = v.findViewById<TextView>(R.id.text)!!
|
||||||
|
|
||||||
|
override fun getSwipeableContainerView() = container
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
|
||||||
|
swipeableViewContainerOnClickListener =
|
||||||
|
View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
|
||||||
|
|
||||||
|
// SwipeableItemAdapter requires stable ID, and also
|
||||||
|
// have to implement the getItemId() method appropriately.
|
||||||
|
setHasStableIds(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(item: Conversation) {
|
||||||
|
data.add(item)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFirst(item: Conversation) {
|
||||||
|
val index = data.size
|
||||||
|
data.addFirst(item)
|
||||||
|
notifyItemInserted(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAll(items: Collection<Conversation>) {
|
||||||
|
val index = data.size
|
||||||
|
data.addAll(items)
|
||||||
|
notifyItemRangeInserted(index, items.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(item: Conversation) {
|
||||||
|
val index = data.indexOf(item)
|
||||||
|
data.removeAll { it.id == item.id }
|
||||||
|
notifyItemRemoved(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(item: Conversation) {
|
||||||
|
val index = data.indexOfFirst { it.id == item.id }
|
||||||
|
if (index >= 0) {
|
||||||
|
data[index] = item
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear(newLabel: Label?) {
|
||||||
|
label = newLabel
|
||||||
|
data.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onItemViewClick(v: View) {
|
||||||
|
eventListener?.onItemViewClicked(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSwipeableViewContainerClick(v: View) {
|
||||||
|
eventListener?.onItemViewClicked(
|
||||||
|
RecyclerViewAdapterUtils.getParentViewHolderItemView(v)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItem(position: Int) = data[position]
|
||||||
|
|
||||||
|
override fun getItemId(position: Int) = data[position].id.leastSignificantBits
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val v = inflater.inflate(R.layout.message_row, parent, false)
|
||||||
|
return ViewHolder(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val item = data[position]
|
||||||
|
|
||||||
|
holder.apply {
|
||||||
|
if (activateOnItemClick) {
|
||||||
|
container.setBackgroundResource(
|
||||||
|
if (position == selectedPosition)
|
||||||
|
R.drawable.bg_item_selected_state
|
||||||
|
else
|
||||||
|
R.drawable.bg_item_normal_state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set listeners
|
||||||
|
// (if the item is *pinned*, click event comes to the itemView)
|
||||||
|
itemView.setOnClickListener(itemViewOnClickListener)
|
||||||
|
// (if the item is *not pinned*, click event comes to the container)
|
||||||
|
container.setOnClickListener(swipeableViewContainerOnClickListener)
|
||||||
|
|
||||||
|
// set data
|
||||||
|
avatar.setImageDrawable(MultiIdenticon(item.participants))
|
||||||
|
|
||||||
|
sender.text = item.participants.mapNotNull { it.alias }.joinToString()
|
||||||
|
subject.text = prepareMessageExtract(item.subject)
|
||||||
|
extract.text = prepareMessageExtract(item.extract)
|
||||||
|
if (item.hasUnread()) {
|
||||||
|
sender.typeface = Typeface.DEFAULT_BOLD
|
||||||
|
subject.typeface = Typeface.DEFAULT_BOLD
|
||||||
|
} else {
|
||||||
|
sender.typeface = Typeface.DEFAULT
|
||||||
|
subject.typeface = Typeface.DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = data.size
|
||||||
|
|
||||||
|
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
|
||||||
|
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
|
||||||
|
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
|
||||||
|
} else {
|
||||||
|
REACTION_CAN_SWIPE_BOTH_H
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SwitchIntDef")
|
||||||
|
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
|
||||||
|
holder.itemView.setBackgroundResource(
|
||||||
|
when (type) {
|
||||||
|
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
|
||||||
|
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
|
||||||
|
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
|
||||||
|
R.drawable.bg_swipe_item_neutral
|
||||||
|
} else {
|
||||||
|
R.drawable.bg_swipe_item_right
|
||||||
|
}
|
||||||
|
else -> R.drawable.bg_swipe_item_neutral
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@SuppressLint("SwitchIntDef")
|
||||||
|
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
|
||||||
|
when (result) {
|
||||||
|
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
|
||||||
|
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
|
||||||
|
|
||||||
|
fun setSelectedPosition(selectedPosition: Int) {
|
||||||
|
val oldPosition = this.selectedPosition
|
||||||
|
this.selectedPosition = selectedPosition
|
||||||
|
notifyItemChanged(oldPosition)
|
||||||
|
notifyItemChanged(selectedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SwipeLeftResultAction internal constructor(
|
||||||
|
adapter: SwipeableConversationAdapter,
|
||||||
|
position: Int
|
||||||
|
) : SwipeResultActionMoveToSwipedDirection() {
|
||||||
|
private var adapter: SwipeableConversationAdapter? = adapter
|
||||||
|
private val item = adapter.data[position]
|
||||||
|
|
||||||
|
override fun onPerformAction() {
|
||||||
|
adapter?.eventListener?.onItemDeleted(item)
|
||||||
|
adapter?.remove(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleanUp() {
|
||||||
|
adapter = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SwipeRightResultAction internal constructor(
|
||||||
|
adapter: SwipeableConversationAdapter,
|
||||||
|
position: Int
|
||||||
|
) : SwipeResultActionRemoveItem() {
|
||||||
|
private var adapter: SwipeableConversationAdapter? = adapter
|
||||||
|
private val item = adapter.data[position]
|
||||||
|
|
||||||
|
override fun onPerformAction() {
|
||||||
|
adapter?.eventListener?.onItemArchived(item)
|
||||||
|
adapter?.remove(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleanUp() {
|
||||||
|
adapter = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -40,11 +40,12 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
|
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
|
||||||
|
|
||||||
override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) {
|
override fun findMessages(label: Label?, offset: Int, limit: Int) =
|
||||||
super.findMessages(null as Label?, offset, limit)
|
if (label === LABEL_ARCHIVE) {
|
||||||
} else {
|
super.findMessages(null as Label?, offset, limit)
|
||||||
super.findMessages(label, offset, limit)
|
} else {
|
||||||
}
|
super.findMessages(label, offset, limit)
|
||||||
|
}
|
||||||
|
|
||||||
override fun countUnread(label: Label?) = when {
|
override fun countUnread(label: Label?) = when {
|
||||||
label === LABEL_ARCHIVE -> 0
|
label === LABEL_ARCHIVE -> 0
|
||||||
@ -63,7 +64,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
|||||||
).toInt()
|
).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findConversations(label: Label?): List<UUID> {
|
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> {
|
||||||
val projection = arrayOf(COLUMN_CONVERSATION)
|
val projection = arrayOf(COLUMN_CONVERSATION)
|
||||||
|
|
||||||
val where = when {
|
val where = when {
|
||||||
@ -74,8 +75,12 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
|||||||
val result = LinkedList<UUID>()
|
val result = LinkedList<UUID>()
|
||||||
sql.readableDatabase.query(
|
sql.readableDatabase.query(
|
||||||
true,
|
true,
|
||||||
TABLE_NAME, projection, where,
|
TABLE_NAME,
|
||||||
null, null, null, null, null
|
projection,
|
||||||
|
where,
|
||||||
|
null, null, null,
|
||||||
|
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
|
||||||
|
if (limit == 0) null else "$offset, $limit"
|
||||||
).use { c ->
|
).use { c ->
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
|
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
|
||||||
@ -133,7 +138,22 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
|||||||
|
|
||||||
// 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.
|
||||||
val projection = arrayOf(COLUMN_ID, COLUMN_IV, COLUMN_TYPE, COLUMN_SENDER, COLUMN_RECIPIENT, COLUMN_DATA, COLUMN_ACK_DATA, COLUMN_SENT, COLUMN_RECEIVED, COLUMN_STATUS, COLUMN_TTL, COLUMN_RETRIES, COLUMN_NEXT_TRY, COLUMN_CONVERSATION)
|
val projection = arrayOf(
|
||||||
|
COLUMN_ID,
|
||||||
|
COLUMN_IV,
|
||||||
|
COLUMN_TYPE,
|
||||||
|
COLUMN_SENDER,
|
||||||
|
COLUMN_RECIPIENT,
|
||||||
|
COLUMN_DATA,
|
||||||
|
COLUMN_ACK_DATA,
|
||||||
|
COLUMN_SENT,
|
||||||
|
COLUMN_RECEIVED,
|
||||||
|
COLUMN_STATUS,
|
||||||
|
COLUMN_TTL,
|
||||||
|
COLUMN_RETRIES,
|
||||||
|
COLUMN_NEXT_TRY,
|
||||||
|
COLUMN_CONVERSATION
|
||||||
|
)
|
||||||
|
|
||||||
sql.readableDatabase.query(
|
sql.readableDatabase.query(
|
||||||
TABLE_NAME, projection,
|
TABLE_NAME, projection,
|
||||||
@ -174,7 +194,8 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
|||||||
labels = findLabels(id!!)
|
labels = findLabels(id!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findLabels(msgId: Any) = (ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
|
private fun findLabels(msgId: Any) =
|
||||||
|
(ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
|
||||||
|
|
||||||
override fun save(message: Plaintext) {
|
override fun save(message: Plaintext) {
|
||||||
saveContactIfNecessary(message.from)
|
saveContactIfNecessary(message.from)
|
||||||
|
Loading…
Reference in New Issue
Block a user