1
0
mirror of https://github.com/TwidereProject/Twidere-Android synced 2025-02-02 09:46:51 +01:00

improved activity filter

This commit is contained in:
Mariotaku Lee 2017-04-29 14:55:11 +08:00
parent 31086e7718
commit a08759ef12
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
11 changed files with 173 additions and 116 deletions

View File

@ -101,7 +101,7 @@ public class ParcelableActivity extends ParcelableStatus implements Parcelable {
@JsonField(name = "has_following_source")
public boolean has_following_source = true;
public transient UserKey[] after_filtered_source_keys;
public transient ParcelableLiteUser[] after_filtered_sources;
public ParcelableActivity() {
}

View File

@ -59,6 +59,10 @@ public class ParcelableLiteUser implements Parcelable {
@CursorField(CachedUsers.PROFILE_IMAGE_URL)
public String profile_image_url;
@JsonField(name = "is_following")
@CursorField(CachedUsers.IS_FOLLOWING)
public boolean is_following;
@Override
public int describeContents() {
return 0;

View File

@ -21,11 +21,9 @@ package org.mariotaku.twidere.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.database.CursorIndexOutOfBoundsException
import android.support.v4.widget.Space
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -47,9 +45,11 @@ import org.mariotaku.twidere.annotation.Referral
import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.fragment.CursorActivitiesFragment
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.util.ParcelableActivityUtils
import org.mariotaku.twidere.model.util.activityStatus
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.util.IntentUtils
import org.mariotaku.twidere.util.JsonSerializer
import org.mariotaku.twidere.util.OnLinkClickHandler
import org.mariotaku.twidere.util.TwidereLinkify
import org.mariotaku.twidere.view.holder.*
@ -133,45 +133,27 @@ class ParcelableActivitiesAdapter(
private var filteredUserKeys: Array<UserKey>? = null
private val gapLoadingIds: MutableSet<ObjectId> = HashSet()
private val reuseActivity = ParcelableActivity()
private val filterIdCache = SparseArray<Array<UserKey>?>()
private var infoCache: Array<ActivityInfo?>? = null
init {
eventListener = EventListener(this)
statusAdapterDelegate.updateOptions()
setHasStableIds(true)
}
override fun isGapItem(position: Int): Boolean {
val dataPosition = position - activityStartIndex
val activityCount = getActivityCount(false)
if (dataPosition < 0 || dataPosition >= activityCount) return false
// Don't show gap if it's last item
if (dataPosition == activityCount - 1) {
return false
}
if (data is ObjectCursor) {
val cursor = (data as ObjectCursor).cursor
if (!cursor.moveToPosition(dataPosition)) return false
val indices = (data as ObjectCursor).indices
return cursor.getInt(indices[Activities.IS_GAP]) == 1
}
return data!![dataPosition].is_gap
return getFieldValue(position, readInfoValueAction = {
it.gap
}, readStatusValueAction = { activity ->
activity.is_gap
}, defValue = false, raw = false)
}
override fun getItemId(position: Int): Long {
val countIndex = itemCounts.getItemCountIndex(position)
when (countIndex) {
ITEM_INDEX_ACTIVITY -> {
return getFieldValue(position, readCursorValueAction = { cursor, indices ->
val accountKey = UserKey.valueOf(cursor.getString(indices[Activities.ACCOUNT_KEY]))
val timestamp = cursor.getLong(indices[Activities.TIMESTAMP])
val maxPosition = cursor.getLong(indices[Activities.MAX_SORT_POSITION])
val minPosition = cursor.getLong(indices[Activities.MIN_SORT_POSITION])
ParcelableActivity.calculateHashCode(accountKey, timestamp, maxPosition,
minPosition)
}, readStatusValueAction = { activity ->
ParcelableActivity.calculateHashCode(activity.account_key, activity.timestamp,
activity.max_sort_position, activity.min_sort_position)
}, defValue = -1, raw = false).toLong()
return getRowId(position, false)
}
else -> {
return (countIndex.toLong() shl 32) or getItemViewType(position).toLong()
@ -193,6 +175,7 @@ class ParcelableActivitiesAdapter(
filteredUserKeys = data.filteredUserIds
}
this.data = data
infoCache = if (data != null) arrayOfNulls(data.size) else null
gapLoadingIds.clear()
updateItemCount()
notifyDataSetChanged()
@ -241,8 +224,8 @@ class ParcelableActivitiesAdapter(
}
ITEM_VIEW_TYPE_TITLE_SUMMARY -> {
val activity = getActivityInternal(position, raw = false, reuse = true)
activity.after_filtered_source_keys = getAfterFilteredSourceKeys(position, false,
activity.source_keys)
val sources = getAfterFilteredSources(position, false)
activity.after_filtered_sources = sources
(holder as ActivityTitleSummaryViewHolder).displayActivity(activity)
}
ITEM_VIEW_TYPE_STUB -> {
@ -275,11 +258,9 @@ class ParcelableActivitiesAdapter(
Activity.Action.MEDIA_TAGGED, Activity.Action.RETWEETED_MEDIA_TAGGED,
Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> {
if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY
filteredUserKeys?.let {
val afterFiltered = getAfterFilteredSourceKeys(position, false)
if (afterFiltered != null && afterFiltered.isEmpty()) {
return ITEM_VIEW_TYPE_EMPTY
}
val afterFiltered = getAfterFilteredSources(position, false)
if (afterFiltered != null && afterFiltered.isEmpty()) {
return ITEM_VIEW_TYPE_EMPTY
}
return ITEM_VIEW_TYPE_TITLE_SUMMARY
}
@ -324,38 +305,29 @@ class ParcelableActivitiesAdapter(
}
fun getTimestamp(adapterPosition: Int, raw: Boolean = false): Long {
return getFieldValue(adapterPosition, readCursorValueAction = { cursor, indices ->
cursor.safeGetLong(indices[Activities.TIMESTAMP])
return getFieldValue(adapterPosition, readInfoValueAction = {
it.timestamp
}, readStatusValueAction = { activity ->
activity.timestamp
}, defValue = -1, raw = raw)
}
fun getAction(adapterPosition: Int, raw: Boolean = false): String? {
return getFieldValue(adapterPosition, readCursorValueAction = { cursor, indices ->
cursor.getString(indices[Activities.ACTION])
return getFieldValue(adapterPosition, readInfoValueAction = {
it.action
}, readStatusValueAction = { activity ->
activity.action
}, defValue = null, raw = raw)
}
fun getRowId(adapterPosition: Int, raw: Boolean = false): Long {
return getFieldValue(adapterPosition, readCursorValueAction = { cursor, indices ->
cursor.safeGetLong(indices[Activities._ID])
return getFieldValue(adapterPosition, readInfoValueAction = {
it._id
}, readStatusValueAction = { activity ->
activity._id
activity.hashCode().toLong()
}, defValue = -1L, raw = raw)
}
fun getSourceKeys(adapterPosition: Int, raw: Boolean = false): Array<UserKey>? {
return getFieldValue(adapterPosition, readCursorValueAction = { cursor, indices ->
cursor.getString(indices[Activities.SOURCE_KEYS])?.let(UserKey::arrayOf)
}, readStatusValueAction = { activity ->
activity.source_keys
}, defValue = null, raw = raw)
}
fun getData(): List<ParcelableActivity>? {
return data
}
@ -375,41 +347,53 @@ class ParcelableActivitiesAdapter(
val data = this.data!!
if (reuse && data is ObjectCursor) {
val activity = data.setInto(dataPosition, reuseActivity)
// activity.after_filtered_source_ids = filterIdCache[activity._id]
// activity.after_filtered_sources = null
activity.after_filtered_sources = null
return activity
} else {
return data[dataPosition]
}
}
private fun getAfterFilteredSourceKeys(position: Int, raw: Boolean,
sourceKeys: Array<UserKey>? = getSourceKeys(position, raw)): Array<UserKey>? {
return filterIdCache[position] ?: run {
val allFiltered = filteredUserKeys
val keys = if (allFiltered != null) {
sourceKeys?.filterNot { it in allFiltered }?.toTypedArray()
} else {
sourceKeys
}
filterIdCache.put(position, keys)
return@run keys
}
private fun getAfterFilteredSources(position: Int, raw: Boolean): Array<ParcelableLiteUser>? {
return getFieldValue(position, readInfoValueAction = {
it.filteredSources
}, readStatusValueAction = lambda2@ { activity ->
if (activity.after_filtered_sources != null) return@lambda2 activity.after_filtered_sources
val sources = ParcelableActivityUtils.filterSources(activity.sources_lite,
filteredUserKeys, followingOnly)
activity.after_filtered_sources = sources
return@lambda2 sources
}, defValue = null, raw = raw)
}
private inline fun <T> getFieldValue(position: Int,
readCursorValueAction: (cursor: Cursor, indices: ObjectCursor.CursorIndices<ParcelableActivity>) -> T,
readInfoValueAction: (ActivityInfo) -> T,
readStatusValueAction: (status: ParcelableActivity) -> T,
defValue: T, raw: Boolean = false): T {
val data = data
if (data is ObjectCursor) {
val dataPosition = position - activityStartIndex
if (dataPosition < 0 || dataPosition >= getActivityCount(true)) {
throw CursorIndexOutOfBoundsException("index: $position, valid range is $0..${getActivityCount(true)}")
}
val cursor = (data as ObjectCursor).cursor
val cursor = data.cursor
if (!cursor.safeMoveToPosition(dataPosition)) return defValue
val indices = (data as ObjectCursor).indices
return readCursorValueAction(cursor, indices)
val indices = data.indices
val info = infoCache?.get(dataPosition) ?: run {
val _id = cursor.safeGetLong(indices[Activities._ID])
val timestamp = cursor.safeGetLong(indices[Activities.TIMESTAMP])
val action = cursor.getString(indices[Activities.ACTION])
val gap = cursor.getInt(indices[Activities.IS_GAP]) == 1
val sources = cursor.getString(indices[Activities.SOURCES_LITE])?.let {
JsonSerializer.parseArray(it, ParcelableLiteUser::class.java)
}
val filteredSources = ParcelableActivityUtils.filterSources(sources, filteredUserKeys,
followingOnly)
val newInfo = ActivityInfo(_id, timestamp, gap, action, filteredSources)
infoCache?.set(dataPosition, newInfo)
return@run newInfo
}
return readInfoValueAction(info)
}
return readStatusValueAction(getActivityInternal(position, raw, false))
}
@ -514,6 +498,14 @@ class ParcelableActivitiesAdapter(
}
}
data class ActivityInfo(
val _id: Long,
val timestamp: Long,
val gap: Boolean,
val action: String,
val filteredSources: Array<ParcelableLiteUser>?
)
companion object {
const val ITEM_VIEW_TYPE_STUB = 0
const val ITEM_VIEW_TYPE_GAP = 1

View File

@ -0,0 +1,39 @@
/*
* 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.api
import org.mariotaku.ktextension.subArray
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.mastodon.model.Relationship
fun Mastodon.batchGetRelationships(ids: Collection<String>): Map<String, Relationship> {
val list = ids.toList()
val indices = ids.indices
val result = HashMap<String, Relationship>()
@Suppress("LoopToCallChain")
for (i in indices step 100) {
val batch = list.subArray(i until (i + 100).coerceAtMost(indices.last))
getRelationships(batch).forEach {
result[it.id] = it
}
}
return result
}

View File

@ -42,6 +42,7 @@ fun ParcelableUser.toLite(): ParcelableLiteUser {
result.screen_name = screen_name
result.name = name
result.profile_image_url = profile_image_url
result.is_following = is_following
return result
}

View File

@ -20,6 +20,7 @@
package org.mariotaku.twidere.extension.model.api.mastodon
import org.mariotaku.microblog.library.mastodon.model.Account
import org.mariotaku.microblog.library.mastodon.model.Relationship
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.api.isHtml
import org.mariotaku.twidere.extension.model.api.spanItems
@ -34,13 +35,15 @@ import org.mariotaku.twidere.util.emoji.EmojioneTranslator
* Created by mariotaku on 2017/4/18.
*/
fun Account.toParcelable(details: AccountDetails, position: Long = 0): ParcelableUser {
return toParcelable(details.key, position).apply {
fun Account.toParcelable(details: AccountDetails, position: Long = 0,
relationship: Relationship? = null): ParcelableUser {
return toParcelable(details.key, position, relationship).apply {
account_color = details.color
}
}
fun Account.toParcelable(accountKey: UserKey, position: Long = 0): ParcelableUser {
fun Account.toParcelable(accountKey: UserKey, position: Long = 0,
relationship: Relationship? = null): ParcelableUser {
val obj = ParcelableUser()
obj.position = position
obj.account_key = accountKey
@ -68,6 +71,17 @@ fun Account.toParcelable(accountKey: UserKey, position: Long = 0): ParcelableUse
obj.listed_count = -1
obj.media_count = -1
obj.user_type = AccountType.MASTODON
val extras = ParcelableUser.Extras()
if (relationship != null && relationship.id == id) {
obj.is_following = relationship.isFollowing
obj.is_follow_request_sent = relationship.isRequested
extras.followed_by = relationship.isFollowedBy
extras.muting = relationship.isMuting
extras.blocking = relationship.isBlocking
}
return obj
}

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.extension.model.api.mastodon
import org.mariotaku.ktextension.mapToArray
import org.mariotaku.microblog.library.mastodon.model.Notification
import org.mariotaku.microblog.library.mastodon.model.Relationship
import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.twidere.extension.model.toLite
import org.mariotaku.twidere.extension.model.toSummaryLine
@ -29,13 +30,15 @@ import org.mariotaku.twidere.model.ParcelableActivity
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
fun Notification.toParcelable(details: AccountDetails): ParcelableActivity {
return toParcelable(details.key).apply {
fun Notification.toParcelable(details: AccountDetails, relationships: Map<String, Relationship>?):
ParcelableActivity {
return toParcelable(details.key, relationships).apply {
account_color = details.color
}
}
fun Notification.toParcelable(accountKey: UserKey): ParcelableActivity {
fun Notification.toParcelable(accountKey: UserKey, relationships: Map<String, Relationship>?):
ParcelableActivity {
val result = ParcelableActivity()
result.account_key = accountKey
result.id = "$id-$id"
@ -45,7 +48,7 @@ fun Notification.toParcelable(accountKey: UserKey): ParcelableActivity {
result.min_sort_position = result.timestamp
result.max_sort_position = result.timestamp
result.sources = toSources(accountKey)
result.sources = toSources(accountKey, relationships)
result.user_key = result.sources?.firstOrNull()?.key ?: UserKey("multiple", null)
when (type) {
@ -79,8 +82,10 @@ fun Notification.toParcelable(accountKey: UserKey): ParcelableActivity {
return result
}
private fun Notification.toSources(accountKey: UserKey): Array<ParcelableUser>? {
private fun Notification.toSources(accountKey: UserKey, relationships: Map<String, Relationship>?):
Array<ParcelableUser>? {
val account = this.account ?: return null
return arrayOf(account.toParcelable(accountKey))
val relationship = relationships?.get(account.id)
return arrayOf(account.toParcelable(accountKey, relationship = relationship))
}

View File

@ -1,6 +1,5 @@
package org.mariotaku.twidere.model.util
import org.mariotaku.twidere.model.ParcelableActivity
import org.mariotaku.twidere.model.ParcelableLiteUser
import org.mariotaku.twidere.model.UserKey
@ -15,42 +14,25 @@ object ParcelableActivityUtils {
/**
* @param activity Activity for processing
* *
* @param filteredUserKeys Those ids will be removed from source_ids.
* @param filtered Those ids will be removed from source_ids.
* *
* @param followingOnly Limit following users in sources
* *
* @return true if source ids changed, false otherwise
*/
fun initAfterFilteredSourceIds(activity: ParcelableActivity, filteredUserKeys: Array<UserKey>,
followingOnly: Boolean): Boolean {
if (activity.sources == null) return false
if (activity.after_filtered_source_keys != null) return false
if (followingOnly || filteredUserKeys.isNotEmpty()) {
val list = activity.sources.filter { user ->
if (followingOnly && !user.is_following) {
return@filter false
}
fun filterSources(sources: Array<ParcelableLiteUser>?, filtered: Array<UserKey>?,
followingOnly: Boolean): Array<ParcelableLiteUser>? {
return sources?.filterNot { user ->
if (filtered != null && user.key in filtered) {
return@filterNot true
}
if (!filteredUserKeys.contains(user.key)) {
return@filter true
}
return@filter false
}.map { it.key }
activity.after_filtered_source_keys = list.toTypedArray()
return true
} else {
activity.after_filtered_source_keys = activity.source_keys
return false
}
}
fun getAfterFilteredSources(activity: ParcelableActivity): Array<ParcelableLiteUser> {
val sources = activity.sources_lite ?: return emptyArray()
val afterFilteredKeys = activity.after_filtered_source_keys?.takeIf {
it.size != sources.size
} ?: return sources
return sources.filter { it.key in afterFilteredKeys }.toTypedArray()
if (followingOnly && !user.is_following) {
return@filterNot true
}
return@filterNot false
}?.toTypedArray()
}

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.task.twitter
import android.content.Context
import android.net.Uri
import android.support.v4.util.ArraySet
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
@ -29,6 +30,7 @@ import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.AccountType
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.isOfficial
@ -60,9 +62,23 @@ class GetActivitiesAboutMeTask(context: Context) : GetActivitiesTask(context) {
when (account.type) {
AccountType.MASTODON -> {
val mastodon = account.newMicroBlogInstance(context, Mastodon::class.java)
return mastodon.getNotifications(paging).map {
it.toParcelable(account)
val notifications = mastodon.getNotifications(paging)
val allUsers = notifications.flatMap {
val user = it.account
val statusUser = it.status?.account
return@flatMap when {
user != null && statusUser != null -> listOf(user, statusUser)
user != null -> listOf(user)
statusUser != null -> listOf(statusUser)
else -> emptyList()
}
}
val userIds = allUsers.mapTo(ArraySet<String>()) { it.id }
val relationships = mastodon.batchGetRelationships(userIds)
val activities = notifications.map {
it.toParcelable(account, relationships)
}
return activities
}
AccountType.TWITTER -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)

View File

@ -207,9 +207,9 @@ class ContentNotificationManager(
if (FilterQueryBuilder.isFiltered(cr, activity)) {
return@forEachRow false
}
ParcelableActivityUtils.initAfterFilteredSourceIds(activity, filteredUserKeys,
pref.isNotificationFollowingOnly)
val sources = ParcelableActivityUtils.getAfterFilteredSources(activity)
val sources = ParcelableActivityUtils.filterSources(activity.sources_lite,
filteredUserKeys, pref.isNotificationFollowingOnly) ?: activity.sources_lite
?: return@forEachRow false
if (sources.isEmpty()) return@forEachRow false

View File

@ -33,7 +33,6 @@ import org.mariotaku.twidere.extension.loadProfileImage
import org.mariotaku.twidere.model.ActivityTitleSummaryMessage
import org.mariotaku.twidere.model.ParcelableActivity
import org.mariotaku.twidere.model.ParcelableLiteUser
import org.mariotaku.twidere.model.util.ParcelableActivityUtils
import org.mariotaku.twidere.view.BadgeView
import org.mariotaku.twidere.view.IconActionView
import org.mariotaku.twidere.view.ProfileImageView
@ -80,7 +79,12 @@ class ActivityTitleSummaryViewHolder(
fun displayActivity(activity: ParcelableActivity) {
val context = adapter.context
val sources = ParcelableActivityUtils.getAfterFilteredSources(activity)
val sources = (activity.after_filtered_sources ?: activity.sources_lite).takeIf {
it.isNotEmpty()
} ?: run {
showNotSupported()
return
}
val message = ActivityTitleSummaryMessage.get(context, adapter.userColorNameManager,
activity, sources, activityTypeView.defaultColor, adapter.useStarsForLikes,
adapter.isNameFirst)