improved hashtag auto completion

supports double hashtag for fanfou
This commit is contained in:
Mariotaku Lee 2017-05-14 01:44:33 +08:00
parent a3adb9a0aa
commit 0eb13ade81
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
14 changed files with 139 additions and 49 deletions

View File

@ -90,7 +90,11 @@ class ComposeAutoCompleteAdapter(context: Context, val requestManager: RequestMa
icon.clearColorFilter()
} else {
text1.spannable = "#${cursor.getString(indices.title)}"
text1.spannable = if (account?.type == AccountType.FANFOU) {
"#${cursor.getString(indices.title)}#"
} else {
"#${cursor.getString(indices.title)}"
}
text2.setText(R.string.hashtag)
icon.setImageResource(R.drawable.ic_action_hashtag)
@ -111,6 +115,9 @@ class ComposeAutoCompleteAdapter(context: Context, val requestManager: RequestMa
val indices = this.indices!!
when (cursor.getString(indices.type)) {
Suggestions.AutoComplete.TYPE_HASHTAGS -> {
if (account?.type == AccountType.FANFOU) {
return "#${cursor.getString(indices.value)}#"
}
return "#${cursor.getString(indices.value)}"
}
Suggestions.AutoComplete.TYPE_USERS -> {

View File

@ -0,0 +1,27 @@
/*
* 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.extension
import android.net.Uri
import okhttp3.HttpUrl
fun HttpUrl.toUri() : Uri {
return Uri.parse(toString())
}

View File

@ -2,6 +2,7 @@ package org.mariotaku.twidere.extension.model
import org.mariotaku.ktextension.addAllTo
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.util.UriUtils
val ParcelableStatus.originalId: String
@ -70,6 +71,23 @@ fun ParcelableStatus.toSummaryLine(): ParcelableActivity.SummaryLine {
return result
}
fun ParcelableStatus.extractFanfouHashtags(): List<String> {
return spans?.filter { span ->
var link = span.link
if (link.startsWith("/")) {
link = "http://fanfou.com$link"
}
if (UriUtils.getAuthority(link) != "fanfou.com") {
return@filter false
}
if (span.start <= 0 || span.end > text_unescaped.lastIndex) return@filter false
if (text_unescaped[span.start - 1] == '#' && text_unescaped[span.end] == '#') {
return@filter true
}
return@filter false
}?.map { text_unescaped.substring(it.start, it.end) }.orEmpty()
}
private fun parcelableUserMention(key: UserKey, name: String, screenName: String) = ParcelableUserMention().also {
it.key = key
it.name = name

View File

@ -23,4 +23,4 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableUser
data class GetTimelineResult<out T>(val account: AccountDetails, val data: List<T>,
val users: Collection<ParcelableUser>)
val users: Collection<ParcelableUser>, val hashtags: Collection<String>)

View File

@ -3,6 +3,8 @@ package org.mariotaku.twidere.task.cache
import android.annotation.SuppressLint
import android.content.Context
import android.support.v4.util.ArraySet
import org.mariotaku.ktextension.ContentValues
import org.mariotaku.ktextension.set
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.extension.bulkInsert
import org.mariotaku.twidere.extension.model.applyTo
@ -10,26 +12,30 @@ import org.mariotaku.twidere.extension.model.relationship
import org.mariotaku.twidere.extension.queryAll
import org.mariotaku.twidere.model.ParcelableRelationship
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.CachedRelationships
import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers
import org.mariotaku.twidere.model.task.GetTimelineResult
import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.task.BaseAbstractTask
import org.mariotaku.twidere.util.content.ContentResolverUtils
class CacheUserRelationshipTask(
class CacheTimelineResultTask(
context: Context,
val accountKey: UserKey,
val accountType: String,
val users: Collection<ParcelableUser>,
val result: GetTimelineResult<*>,
val cacheRelationship: Boolean
) : BaseAbstractTask<Any?, Unit, Any?>(context) {
override fun doLongOperation(param: Any?) {
val cr = context.contentResolver
cr.bulkInsert(CachedUsers.CONTENT_URI, users, ParcelableUser::class.java)
val account = result.account
val users = result.users
val hashtags = result.hashtags
cr.bulkInsert(CachedUsers.CONTENT_URI, users, ParcelableUser::class.java)
ContentResolverUtils.bulkInsert(cr, CachedHashtags.CONTENT_URI, hashtags.map {
ContentValues { this[CachedHashtags.NAME] = it.substringAfter("#") }
})
if (cacheRelationship) {
val selectionArgsList = users.mapTo(mutableListOf(accountKey.toString())) {
val selectionArgsList = users.mapTo(mutableListOf(account.key.toString())) {
it.key.toString()
}
@SuppressLint("Recycle")

View File

@ -34,6 +34,7 @@ import org.mariotaku.twidere.annotation.ReadPositionTag
import org.mariotaku.twidere.extension.api.batchGetRelationships
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.microblog.toParcelable
import org.mariotaku.twidere.extension.model.extractFanfouHashtags
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.fragment.InteractionsTimelineFragment
@ -84,17 +85,29 @@ class GetActivitiesAboutMeTask(context: Context) : GetActivitiesTask(context) {
}
return GetTimelineResult(account, activities, activities.flatMap {
it.sources?.toList().orEmpty()
}, notifications.flatMapTo(HashSet()) { notification ->
notification.status?.tags?.map { it.name }.orEmpty()
})
}
AccountType.TWITTER -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
if (account.isOfficial(context)) {
val activities = microBlog.getActivitiesAboutMe(paging).map {
val timeline = microBlog.getActivitiesAboutMe(paging)
val activities = timeline.map {
it.toParcelable(account, profileImageSize = profileImageSize)
}
return GetTimelineResult(account, activities, activities.flatMap {
it.sources?.toList().orEmpty()
}, timeline.flatMapTo(HashSet()) { activity ->
val mapResult = mutableSetOf<String>()
activity.targetStatuses?.flatMapTo(mapResult) { status ->
status.entities?.hashtags?.map { it.text }.orEmpty()
}
activity.targetObjectStatuses?.flatMapTo(mapResult) { status ->
status.entities?.hashtags?.map { it.text }.orEmpty()
}
return@flatMapTo mapResult
})
}
}
@ -106,16 +119,19 @@ class GetActivitiesAboutMeTask(context: Context) : GetActivitiesTask(context) {
}
return GetTimelineResult(account, activities, activities.flatMap {
it.sources?.toList().orEmpty()
})
}, activities.flatMap { it.extractFanfouHashtags() })
}
}
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val activities = microBlog.getMentionsTimeline(paging).map {
val timeline = microBlog.getMentionsTimeline(paging)
val activities = timeline.map {
InternalActivityCreator.status(it, account.key.id).toParcelable(account,
profileImageSize = profileImageSize)
}
return GetTimelineResult(account, activities, activities.flatMap {
it.sources?.toList().orEmpty()
}, timeline.flatMap {
it.entities?.hashtags?.map { it.text }.orEmpty()
})
}

View File

@ -104,7 +104,7 @@ abstract class GetActivitiesTask(
context.contentResolver.notifyChange(contentUri, null)
val exception = results.firstOrNull { it.second != null }?.second
bus.post(GetActivitiesTaskEvent(contentUri, false, exception))
GetStatusesTask.cacheUserRelationship(context, results)
GetStatusesTask.cacheItems(context, results)
handler?.invoke(true)
}

View File

@ -31,6 +31,7 @@ import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.ReadPositionTag
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.extractFanfouHashtags
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.fragment.HomeTimelineFragment
import org.mariotaku.twidere.model.AccountDetails
@ -66,14 +67,22 @@ class GetHomeTimelineTask(context: Context) : GetStatusesTask(context) {
val mapResult = mutableListOf(status.account.toParcelable(account))
status.reblog?.account?.toParcelable(account)?.addTo(mapResult)
return@flatMap mapResult
}, timeline.flatMap { status ->
status.tags?.map { it.name }.orEmpty()
})
}
else -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val timeline = microBlog.getHomeTimeline(paging)
return GetTimelineResult(account, timeline.map {
val statuses = timeline.map {
it.toParcelable(account, profileImageSize)
}, timeline.flatMap { status ->
}
val hashtags = if (account.type == AccountType.FANFOU) statuses.flatMap { status ->
return@flatMap status.extractFanfouHashtags()
} else timeline.flatMap { status ->
status.entities?.hashtags?.map { it.text }.orEmpty()
}
return GetTimelineResult(account, statuses, timeline.flatMap { status ->
val mapResult = mutableListOf(status.user.toParcelable(account,
profileImageSize = profileImageSize))
status.retweetedStatus?.user?.toParcelable(account,
@ -81,7 +90,7 @@ class GetHomeTimelineTask(context: Context) : GetStatusesTask(context) {
status.quotedStatus?.user?.toParcelable(account,
profileImageSize = profileImageSize)?.addTo(mapResult)
return@flatMap mapResult
})
}, hashtags)
}
}
}

View File

@ -30,7 +30,7 @@ import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.AccountSupportColumns
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.BaseAbstractTask
import org.mariotaku.twidere.task.cache.CacheUserRelationshipTask
import org.mariotaku.twidere.task.cache.CacheTimelineResultTask
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.DebugLog
import org.mariotaku.twidere.util.ErrorInfoStore
@ -115,7 +115,7 @@ abstract class GetStatusesTask(
context.contentResolver.notifyChange(contentUri, null)
val exception = results.firstOrNull { it.second != null }?.second
bus.post(GetStatusesTaskEvent(contentUri, false, exception))
cacheUserRelationship(context, results)
cacheItems(context, results)
handler?.invoke(true)
}
@ -234,11 +234,11 @@ abstract class GetStatusesTask(
return timestamp + (sortId - lastSortId) * (499 - count) / sortDiff + extraValue.toLong()
}
fun cacheUserRelationship(context: Context, results: List<Pair<GetTimelineResult<*>?, Exception?>>) {
fun cacheItems(context: Context, results: List<Pair<GetTimelineResult<*>?, Exception?>>) {
results.forEach { (result, _) ->
if (result == null) return@forEach
val account = result.account
val task = CacheUserRelationshipTask(context, account.key, account.type, result.users,
val task = CacheTimelineResultTask(context, result,
account.type == AccountType.STATUSNET || account.isOfficial(context))
TaskStarter.execute(task)
}

View File

@ -28,6 +28,7 @@ import org.mariotaku.kpreferences.get
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_URI
import org.mariotaku.twidere.constant.phishingLinksWaringKey
import org.mariotaku.twidere.fragment.PhishingLinkWarningDialogFragment
import org.mariotaku.twidere.model.UserKey
class DirectMessageOnLinkClickHandler(
context: Context,
@ -38,10 +39,10 @@ class DirectMessageOnLinkClickHandler(
override val isPrivateData: Boolean
get() = true
override fun openLink(link: String) {
override fun openLink(accountKey: UserKey?, link: String) {
if (manager != null && manager.isActive) return
if (!hasShortenedLinks(link)) {
super.openLink(link)
super.openLink(accountKey, link)
return
}
if (context is FragmentActivity && preferences[phishingLinksWaringKey]) {
@ -52,7 +53,7 @@ class DirectMessageOnLinkClickHandler(
fragment.arguments = args
fragment.show(fm, "phishing_link_warning")
} else {
super.openLink(link)
super.openLink(accountKey, link)
}
}

View File

@ -26,7 +26,6 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.StyleSpan
import android.text.style.URLSpan
import okhttp3.HttpUrl
import org.attoparser.ParseException
import org.attoparser.config.ParseConfiguration
import org.attoparser.simple.AbstractSimpleMarkupHandler
@ -76,10 +75,7 @@ object HtmlSpanBuilder {
private fun createSpan(info: TagInfo): Any? {
when (info.nameLower) {
"a" -> {
var href = info.getAttribute("href") ?: return null
if (HttpUrl.parse(href)?.scheme() == null) {
href = "https://" + href
}
val href = info.getAttribute("href") ?: return null
return URLSpan(href)
}
"b", "strong" -> {

View File

@ -26,14 +26,18 @@ import android.content.SharedPreferences
import android.net.Uri
import android.os.BadParcelableException
import android.support.v4.content.ContextCompat
import okhttp3.HttpUrl
import org.mariotaku.kpreferences.get
import org.mariotaku.twidere.TwidereConstants.USER_TYPE_TWITTER_COM
import org.mariotaku.twidere.activity.WebLinkHandlerActivity
import org.mariotaku.twidere.annotation.Referral
import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_HOST
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEY
import org.mariotaku.twidere.constant.displaySensitiveContentsKey
import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.extension.model.AcctPlaceholderUserKey
import org.mariotaku.twidere.extension.toUri
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.ParcelableMediaUtils
import org.mariotaku.twidere.util.TwidereLinkify.OnLinkClickListener
@ -65,7 +69,7 @@ open class OnLinkClickHandler(
if (accountKey != null && isMedia(link, extraId)) {
openMedia(accountKey, extraId, sensitive, link, start, end)
} else {
openLink(link)
openLink(accountKey, link)
}
return true
}
@ -74,20 +78,15 @@ open class OnLinkClickHandler(
openMedia(accountKey, extraId, sensitive, link, start, end)
} else {
val authority = UriUtils.getAuthority(link)
if (authority == null) {
openLink(link)
if (authority == "fanfou.com") {
if (accountKey != null && handleFanfouLink(link, orig, accountKey)) {
return true
}
} else if (IntentUtils.isWebLinkHandled(context, Uri.parse(link))) {
openTwitterLink(accountKey, link)
return true
}
when (authority) {
"fanfou.com" -> if (accountKey != null && handleFanfouLink(link, orig, accountKey)) {
return true
}
else -> if (IntentUtils.isWebLinkHandled(context, Uri.parse(link))) {
openTwitterLink(link, accountKey!!)
return true
}
}
openLink(link)
openLink(accountKey, link)
}
return true
}
@ -132,17 +131,27 @@ open class OnLinkClickHandler(
preferences[displaySensitiveContentsKey])
}
protected open fun openLink(link: String) {
protected open fun openLink(accountKey: UserKey?, link: String) {
if (manager != null && manager.isActive) return
openLink(context, preferences, Uri.parse(link))
val uri = Uri.parse(link)
if (uri.isRelative && accountKey != null && accountKey.host != null) {
val absUri = HttpUrl.parse("http://${accountKey.host}/").resolve(link).toUri()
openLink(context, preferences, absUri)
return
}
openLink(context, preferences, uri)
}
protected fun openTwitterLink(link: String, accountKey: UserKey) {
protected fun openTwitterLink(accountKey: UserKey?, link: String) {
if (manager != null && manager.isActive) return
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.setClass(context, WebLinkHandlerActivity::class.java)
intent.putExtra(EXTRA_ACCOUNT_KEY, accountKey)
if (accountKey != null) {
intent.putExtra(EXTRA_ACCOUNT_KEY, accountKey)
} else {
intent.putExtra(EXTRA_ACCOUNT_HOST, USER_TYPE_TWITTER_COM)
}
intent.setExtrasClassLoader(TwidereApplication::class.java.classLoader)
if (intent.resolveActivity(context.packageManager) != null) {
try {
@ -199,3 +208,4 @@ open class OnLinkClickHandler(
}
}
}

View File

@ -45,7 +45,7 @@ class StatusAdapterLinkClickHandler<D>(context: Context, preferences: SharedPref
val media = ParcelableMediaUtils.getAllMedia(status)
val current = StatusLinkClickHandler.findByLink(media, link)
if (current != null && current.open_browser) {
openLink(link)
openLink(accountKey, link)
} else {
IntentUtils.openMedia(context, status, current, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])

View File

@ -45,7 +45,7 @@ open class StatusLinkClickHandler(
val status = status
val current = findByLink(status!!.media, link)
if (current == null || current.open_browser) {
openLink(link)
openLink(accountKey, link)
} else {
IntentUtils.openMedia(context, status, current, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])