Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageNewConversationFragm...

361 lines
15 KiB
Kotlin

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.fragment.message
import android.accounts.AccountManager
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.os.Bundle
import android.support.annotation.WorkerThread
import android.support.v4.app.LoaderManager.LoaderCallbacks
import android.support.v4.content.Loader
import android.support.v7.widget.LinearLayoutManager
import android.text.Editable
import android.text.Spannable
import android.text.TextUtils
import android.text.style.ReplacementSpan
import android.view.*
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.fragment_messages_conversation_new.*
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.Bundle
import org.mariotaku.ktextension.set
import org.mariotaku.ktextension.setItemAvailability
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.SelectableUsersAdapter
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.fragment.BaseFragment
import org.mariotaku.twidere.loader.CacheUserSearchLoader
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.model.ParcelableMessageConversation.ConversationType
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.task.twitter.message.SendMessageTask
import org.mariotaku.twidere.text.MarkForDeleteSpan
import org.mariotaku.twidere.util.IntentUtils
import org.mariotaku.twidere.util.view.SimpleTextWatcher
import java.lang.ref.WeakReference
/**
* Created by mariotaku on 2017/2/15.
*/
class MessageNewConversationFragment : BaseFragment(), LoaderCallbacks<List<ParcelableUser>?> {
private val accountKey by lazy { arguments.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY) }
private val account by lazy {
AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true)
}
private val selectedRecipients: List<ParcelableUser>
get() {
val text = editParticipants.editableText ?: return emptyList()
return text.getSpans(0, text.length, ParticipantSpan::class.java).map(ParticipantSpan::user)
}
private var loaderInitialized: Boolean = false
private var performSearchRequestRunnable: Runnable? = null
private lateinit var usersAdapter: SelectableUsersAdapter
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setHasOptionsMenu(true)
usersAdapter = SelectableUsersAdapter(context, Glide.with(this))
recyclerView.adapter = usersAdapter
recyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
editParticipants.addTextChangedListener(object : SimpleTextWatcher {
override fun afterTextChanged(s: Editable) {
s.getSpans(0, s.length, MarkForDeleteSpan::class.java).forEach { span ->
val deleteStart = s.getSpanStart(span)
val deleteEnd = s.getSpanEnd(span)
s.removeSpan(span)
s.delete(deleteStart, deleteEnd)
}
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
super.beforeTextChanged(s, start, count, after)
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s !is Spannable) return
s.getSpans(0, s.length, PendingQuerySpan::class.java).forEach { span ->
s.removeSpan(span)
}
// Processing deletion
if (count < before) {
val spans = s.getSpans(start, start, ParticipantSpan::class.java)
if (spans.isNotEmpty()) {
spans.forEach { span ->
val deleteStart = s.getSpanStart(span)
val deleteEnd = s.getSpanEnd(span)
s.removeSpan(span)
s.setSpan(MarkForDeleteSpan(), deleteStart, deleteEnd,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
updateCheckState()
}
return
}
}
val spaceNextStart = run {
val spaceIdx = s.indexOfLast(Char::isWhitespace)
if (spaceIdx < 0) return@run 0
return@run spaceIdx + 1
}
// Skip if last char is space
if (spaceNextStart > s.lastIndex) return
if (s.getSpans(start, start + count, ParticipantSpan::class.java).isEmpty()) {
s.setSpan(PendingQuerySpan(), spaceNextStart, start + count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
searchUser(s.substring(spaceNextStart), true)
}
}
})
val nameFirst = preferences[nameFirstKey]
val roundRadius = resources.getDimension(R.dimen.element_spacing_xsmall)
val spanPadding = resources.getDimension(R.dimen.element_spacing_xsmall)
usersAdapter.itemCheckedListener = itemChecked@ { pos, checked ->
val text: Editable = editParticipants.editableText ?: return@itemChecked
val user = usersAdapter.getUser(pos) ?: return@itemChecked
if (checked) {
text.getSpans(0, text.length, PendingQuerySpan::class.java).forEach { pending ->
val start = text.getSpanStart(pending)
val end = text.getSpanEnd(pending)
text.removeSpan(pending)
if (start < 0 || end < 0 || end < start) return@forEach
text.delete(start, end)
}
val displayName = userColorNameManager.getDisplayName(user, nameFirst)
val span = ParticipantSpan(user, displayName, roundRadius, spanPadding)
val start = text.length
text.append("${user.screen_name} ")
val end = text.length - 1
text.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} else {
text.getSpans(0, text.length, ParticipantSpan::class.java).forEach { span ->
if (user != span.user) {
return@forEach
}
val start = text.getSpanStart(span)
var end = text.getSpanEnd(span)
text.removeSpan(span)
// Also remove last whitespace
if (end <= text.lastIndex && text[end].isWhitespace()) {
end += 1
}
text.delete(start, end)
}
}
editParticipants.clearComposingText()
updateCheckState()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_messages_conversation_new, container, false)
}
override fun onCreateLoader(id: Int, args: Bundle): Loader<List<ParcelableUser>?> {
val query = args.getString(EXTRA_QUERY)
val fromCache = args.getBoolean(EXTRA_FROM_CACHE)
val fromUser = args.getBoolean(EXTRA_FROM_USER)
return CacheUserSearchLoader(context, accountKey, query, !fromCache, true, fromUser)
}
override fun onLoaderReset(loader: Loader<List<ParcelableUser>?>) {
usersAdapter.data = null
}
override fun onLoadFinished(loader: Loader<List<ParcelableUser>?>, data: List<ParcelableUser>?) {
usersAdapter.data = data
updateCheckState()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_messages_conversation_new, menu)
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.setItemAvailability(R.id.create_conversation, selectedRecipients.isNotEmpty())
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.create_conversation -> {
createConversation()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun createConversation() {
val account = this.account ?: return
val selected = this.selectedRecipients
if (selected.isEmpty()) return
val maxParticipants = if (account.isOfficial(context)) {
defaultFeatures.twitterDirectMessageMaxParticipants
} else {
1
}
if (selected.size > maxParticipants) {
editParticipants.error = getString(R.string.error_message_message_too_many_participants)
return
}
val conversation = ParcelableMessageConversation()
conversation.account_color = account.color
conversation.account_key = account.key
conversation.id = "${SendMessageTask.TEMP_CONVERSATION_ID_PREFIX}${System.currentTimeMillis()}"
conversation.local_timestamp = System.currentTimeMillis()
conversation.conversation_type = if (selected.size > 1) {
ConversationType.GROUP
} else {
ConversationType.ONE_TO_ONE
}
conversation.participants = (selected + account.user).toTypedArray()
conversation.is_temp = true
if (conversation.conversation_type == ConversationType.ONE_TO_ONE) {
val participantKeys = conversation.participants.map(ParcelableUser::key)
val existingConversation = findMessageConversation(context, accountKey, participantKeys)
if (existingConversation != null) {
activity.startActivity(IntentUtils.messageConversation(accountKey, existingConversation.id))
activity.finish()
return
}
}
val values = ObjectCursor.valuesCreatorFrom(ParcelableMessageConversation::class.java).create(conversation)
context.contentResolver.insert(Conversations.CONTENT_URI, values)
activity.startActivity(IntentUtils.messageConversation(accountKey, conversation.id))
activity.finish()
}
private fun updateCheckState() {
val selected = selectedRecipients
usersAdapter.clearCheckState()
usersAdapter.clearLockedState()
usersAdapter.setLockedState(accountKey, true)
selected.forEach { user ->
usersAdapter.setCheckState(user.key, true)
}
usersAdapter.notifyDataSetChanged()
activity?.supportInvalidateOptionsMenu()
}
private fun searchUser(query: String, fromType: Boolean) {
if (TextUtils.isEmpty(query)) {
return
}
val args = Bundle {
this[EXTRA_ACCOUNT_KEY] = accountKey
this[EXTRA_QUERY] = query
this[EXTRA_FROM_CACHE] = fromType
}
if (loaderInitialized) {
loaderManager.initLoader(0, args, this)
loaderInitialized = true
} else {
loaderManager.restartLoader(0, args, this)
}
if (performSearchRequestRunnable != null) {
editParticipants.removeCallbacks(performSearchRequestRunnable)
}
if (fromType) {
performSearchRequestRunnable = PerformSearchRequestRunnable(query, this)
editParticipants.postDelayed(performSearchRequestRunnable, 1000L)
}
}
@WorkerThread
fun findMessageConversation(context: Context, accountKey: UserKey,
participantKeys: Collection<UserKey>): ParcelableMessageConversation? {
val resolver = context.contentResolver
val where = Expression.and(Expression.equalsArgs(Conversations.ACCOUNT_KEY),
Expression.equalsArgs(Conversations.PARTICIPANT_KEYS)).sql
val whereArgs = arrayOf(accountKey.toString(), participantKeys.sorted().joinToString(","))
val cur = resolver.query(Conversations.CONTENT_URI, Conversations.COLUMNS, where, whereArgs, null) ?: return null
try {
if (cur.moveToFirst()) {
val indices = ObjectCursor.indicesFrom(cur, ParcelableMessageConversation::class.java)
return indices.newObject(cur)
}
} finally {
cur.close()
}
return null
}
internal class PerformSearchRequestRunnable(val query: String, fragment: MessageNewConversationFragment) : Runnable {
val fragmentRef = WeakReference(fragment)
override fun run() {
val fragment = fragmentRef.get() ?: return
fragment.searchUser(query, false)
}
}
class PendingQuerySpan
class ParticipantSpan(
val user: ParcelableUser,
val displayName: String,
val roundRadius: Float,
val padding: Float
) : ReplacementSpan() {
private var backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var backgroundBounds = RectF()
private var nameWidth: Float = 0f
init {
backgroundPaint.color = 0x20808080
}
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
backgroundBounds.set(x, top.toFloat() + padding / 2, x + nameWidth + padding * 2, bottom - padding / 2)
canvas.drawRoundRect(backgroundBounds, roundRadius, roundRadius, backgroundPaint)
val textSizeBackup = paint.textSize
paint.textSize = textSizeBackup - padding
canvas.drawText(displayName, x + padding, y - padding / 2, paint)
paint.textSize = textSizeBackup
}
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
val textSizeBackup = paint.textSize
paint.textSize = textSizeBackup - padding
nameWidth = paint.measureText(displayName)
paint.textSize = textSizeBackup
return Math.round(nameWidth + padding * 2)
}
}
}