2017-02-15 09:11:11 +01:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
|
2017-02-15 18:03:48 +01:00
|
|
|
import android.accounts.AccountManager
|
2017-02-26 05:46:19 +01:00
|
|
|
import android.content.Context
|
2017-02-15 14:45:59 +01:00
|
|
|
import android.graphics.Canvas
|
|
|
|
import android.graphics.Paint
|
2017-02-15 15:34:06 +01:00
|
|
|
import android.graphics.RectF
|
2017-02-15 14:45:59 +01:00
|
|
|
import android.os.Bundle
|
2017-02-26 05:46:19 +01:00
|
|
|
import android.support.annotation.WorkerThread
|
2017-02-15 14:45:59 +01:00
|
|
|
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
|
2017-02-15 18:03:48 +01:00
|
|
|
import android.view.*
|
2017-03-01 15:12:25 +01:00
|
|
|
import com.bumptech.glide.Glide
|
2017-02-15 14:45:59 +01:00
|
|
|
import kotlinx.android.synthetic.main.fragment_messages_conversation_new.*
|
|
|
|
import org.mariotaku.kpreferences.get
|
|
|
|
import org.mariotaku.ktextension.Bundle
|
|
|
|
import org.mariotaku.ktextension.set
|
2017-02-15 18:03:48 +01:00
|
|
|
import org.mariotaku.ktextension.setItemAvailability
|
2017-03-05 09:08:09 +01:00
|
|
|
import org.mariotaku.library.objectcursor.ObjectCursor
|
2017-02-26 05:46:19 +01:00
|
|
|
import org.mariotaku.sqliteqb.library.Expression
|
2017-02-15 14:45:59 +01:00
|
|
|
import org.mariotaku.twidere.R
|
|
|
|
import org.mariotaku.twidere.adapter.SelectableUsersAdapter
|
|
|
|
import org.mariotaku.twidere.constant.IntentConstants.*
|
|
|
|
import org.mariotaku.twidere.constant.nameFirstKey
|
2017-02-15 18:03:48 +01:00
|
|
|
import org.mariotaku.twidere.extension.model.isOfficial
|
2017-02-15 09:11:11 +01:00
|
|
|
import org.mariotaku.twidere.fragment.BaseFragment
|
2017-02-15 14:45:59 +01:00
|
|
|
import org.mariotaku.twidere.loader.CacheUserSearchLoader
|
2017-03-05 09:08:09 +01:00
|
|
|
import org.mariotaku.twidere.model.ParcelableMessageConversation
|
2017-02-15 18:03:48 +01:00
|
|
|
import org.mariotaku.twidere.model.ParcelableMessageConversation.ConversationType
|
2017-03-05 09:08:09 +01:00
|
|
|
import org.mariotaku.twidere.model.ParcelableUser
|
|
|
|
import org.mariotaku.twidere.model.UserKey
|
2017-02-15 18:03:48 +01:00
|
|
|
import org.mariotaku.twidere.model.util.AccountUtils
|
|
|
|
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
|
2017-02-17 11:42:39 +01:00
|
|
|
import org.mariotaku.twidere.task.twitter.message.SendMessageTask
|
2017-02-15 14:45:59 +01:00
|
|
|
import org.mariotaku.twidere.text.MarkForDeleteSpan
|
2017-02-15 18:03:48 +01:00
|
|
|
import org.mariotaku.twidere.util.IntentUtils
|
2017-02-15 14:45:59 +01:00
|
|
|
import org.mariotaku.twidere.util.view.SimpleTextWatcher
|
2017-02-15 19:02:59 +01:00
|
|
|
import java.lang.ref.WeakReference
|
2017-02-15 09:11:11 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Created by mariotaku on 2017/2/15.
|
|
|
|
*/
|
2017-02-15 14:45:59 +01:00
|
|
|
class MessageNewConversationFragment : BaseFragment(), LoaderCallbacks<List<ParcelableUser>?> {
|
2017-02-15 09:11:11 +01:00
|
|
|
|
2017-02-15 18:03:48 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2017-02-15 14:45:59 +01:00
|
|
|
private var loaderInitialized: Boolean = false
|
2017-02-15 19:02:59 +01:00
|
|
|
private var performSearchRequestRunnable: Runnable? = null
|
2017-02-15 14:45:59 +01:00
|
|
|
|
|
|
|
private lateinit var usersAdapter: SelectableUsersAdapter
|
|
|
|
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
|
|
super.onActivityCreated(savedInstanceState)
|
2017-02-15 18:03:48 +01:00
|
|
|
setHasOptionsMenu(true)
|
2017-03-02 07:59:19 +01:00
|
|
|
usersAdapter = SelectableUsersAdapter(context, Glide.with(this))
|
2017-02-15 14:45:59 +01:00
|
|
|
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)
|
2017-02-15 19:11:16 +01:00
|
|
|
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
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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()) {
|
2017-02-15 19:11:16 +01:00
|
|
|
s.setSpan(PendingQuerySpan(), spaceNextStart, start + count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
2017-02-15 14:45:59 +01:00
|
|
|
searchUser(s.substring(spaceNextStart), true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
val nameFirst = preferences[nameFirstKey]
|
2017-02-15 15:34:06 +01:00
|
|
|
val roundRadius = resources.getDimension(R.dimen.element_spacing_xsmall)
|
|
|
|
val spanPadding = resources.getDimension(R.dimen.element_spacing_xsmall)
|
2017-02-15 14:45:59 +01:00
|
|
|
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)
|
|
|
|
}
|
2017-02-15 15:34:06 +01:00
|
|
|
val displayName = userColorNameManager.getDisplayName(user, nameFirst)
|
|
|
|
val span = ParticipantSpan(user, displayName, roundRadius, spanPadding)
|
2017-02-15 14:45:59 +01:00
|
|
|
val start = text.length
|
2017-02-17 17:07:57 +01:00
|
|
|
text.append("${user.screen_name} ")
|
|
|
|
val end = text.length - 1
|
2017-02-15 14:45:59 +01:00
|
|
|
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)
|
2017-02-15 18:03:48 +01:00
|
|
|
return CacheUserSearchLoader(context, accountKey, query, !fromCache, true, fromUser)
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoaderReset(loader: Loader<List<ParcelableUser>?>) {
|
|
|
|
usersAdapter.data = null
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoadFinished(loader: Loader<List<ParcelableUser>?>, data: List<ParcelableUser>?) {
|
|
|
|
usersAdapter.data = data
|
|
|
|
updateCheckState()
|
|
|
|
}
|
|
|
|
|
2017-02-15 18:03:48 +01:00
|
|
|
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
|
2017-02-17 11:42:39 +01:00
|
|
|
conversation.id = "${SendMessageTask.TEMP_CONVERSATION_ID_PREFIX}${System.currentTimeMillis()}"
|
2017-02-15 18:03:48 +01:00
|
|
|
conversation.local_timestamp = System.currentTimeMillis()
|
|
|
|
conversation.conversation_type = if (selected.size > 1) {
|
|
|
|
ConversationType.GROUP
|
2017-02-26 05:58:54 +01:00
|
|
|
} else {
|
|
|
|
ConversationType.ONE_TO_ONE
|
2017-02-15 18:03:48 +01:00
|
|
|
}
|
|
|
|
conversation.participants = (selected + account.user).toTypedArray()
|
|
|
|
conversation.is_temp = true
|
2017-02-26 05:46:19 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-05 09:08:09 +01:00
|
|
|
val values = ObjectCursor.valuesCreatorFrom(ParcelableMessageConversation::class.java).create(conversation)
|
2017-02-15 18:03:48 +01:00
|
|
|
context.contentResolver.insert(Conversations.CONTENT_URI, values)
|
|
|
|
activity.startActivity(IntentUtils.messageConversation(accountKey, conversation.id))
|
|
|
|
activity.finish()
|
|
|
|
}
|
|
|
|
|
2017-02-15 14:45:59 +01:00
|
|
|
private fun updateCheckState() {
|
|
|
|
val selected = selectedRecipients
|
|
|
|
usersAdapter.clearCheckState()
|
2017-02-25 16:51:44 +01:00
|
|
|
usersAdapter.clearLockedState()
|
|
|
|
usersAdapter.setLockedState(accountKey, true)
|
2017-02-15 14:45:59 +01:00
|
|
|
selected.forEach { user ->
|
|
|
|
usersAdapter.setCheckState(user.key, true)
|
|
|
|
}
|
|
|
|
usersAdapter.notifyDataSetChanged()
|
2017-02-15 18:03:48 +01:00
|
|
|
activity?.supportInvalidateOptionsMenu()
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
|
2017-02-15 19:02:59 +01:00
|
|
|
|
|
|
|
private fun searchUser(query: String, fromType: Boolean) {
|
2017-02-15 14:45:59 +01:00
|
|
|
if (TextUtils.isEmpty(query)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val args = Bundle {
|
|
|
|
this[EXTRA_ACCOUNT_KEY] = accountKey
|
|
|
|
this[EXTRA_QUERY] = query
|
2017-02-15 19:02:59 +01:00
|
|
|
this[EXTRA_FROM_CACHE] = fromType
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
if (loaderInitialized) {
|
|
|
|
loaderManager.initLoader(0, args, this)
|
|
|
|
loaderInitialized = true
|
|
|
|
} else {
|
|
|
|
loaderManager.restartLoader(0, args, this)
|
|
|
|
}
|
2017-02-15 19:02:59 +01:00
|
|
|
if (performSearchRequestRunnable != null) {
|
|
|
|
editParticipants.removeCallbacks(performSearchRequestRunnable)
|
|
|
|
}
|
|
|
|
if (fromType) {
|
|
|
|
performSearchRequestRunnable = PerformSearchRequestRunnable(query, this)
|
|
|
|
editParticipants.postDelayed(performSearchRequestRunnable, 1000L)
|
|
|
|
}
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
|
2017-02-26 05:46:19 +01:00
|
|
|
|
|
|
|
@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()) {
|
2017-03-05 09:08:09 +01:00
|
|
|
val indices = ObjectCursor.indicesFrom(cur, ParcelableMessageConversation::class.java)
|
|
|
|
return indices.newObject(cur)
|
2017-02-26 05:46:19 +01:00
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
cur.close()
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2017-02-15 19:02:59 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2017-02-15 14:45:59 +01:00
|
|
|
|
2017-02-15 15:34:06 +01:00
|
|
|
class PendingQuerySpan
|
2017-02-15 14:45:59 +01:00
|
|
|
|
2017-02-15 15:34:06 +01:00
|
|
|
class ParticipantSpan(
|
|
|
|
val user: ParcelableUser,
|
|
|
|
val displayName: String,
|
|
|
|
val roundRadius: Float,
|
|
|
|
val padding: Float
|
|
|
|
) : ReplacementSpan() {
|
2017-02-15 14:45:59 +01:00
|
|
|
|
|
|
|
private var backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
2017-02-15 15:34:06 +01:00
|
|
|
private var backgroundBounds = RectF()
|
2017-02-15 14:45:59 +01:00
|
|
|
private var nameWidth: Float = 0f
|
|
|
|
|
|
|
|
init {
|
2017-02-15 15:34:06 +01:00
|
|
|
backgroundPaint.color = 0x20808080
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
|
2017-02-15 15:34:06 +01:00
|
|
|
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
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
|
2017-02-15 15:34:06 +01:00
|
|
|
val textSizeBackup = paint.textSize
|
|
|
|
paint.textSize = textSizeBackup - padding
|
2017-02-15 14:45:59 +01:00
|
|
|
nameWidth = paint.measureText(displayName)
|
2017-02-15 15:34:06 +01:00
|
|
|
paint.textSize = textSizeBackup
|
|
|
|
return Math.round(nameWidth + padding * 2)
|
2017-02-15 14:45:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2017-02-26 05:46:19 +01:00
|
|
|
|
2017-02-15 09:11:11 +01:00
|
|
|
}
|