improved twitter replies #809

This commit is contained in:
Mariotaku Lee 2017-04-29 11:19:31 +08:00
parent fa6d658394
commit 31086e7718
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
15 changed files with 291 additions and 60 deletions

View File

@ -102,8 +102,6 @@ public class ParcelableActivity extends ParcelableStatus implements Parcelable {
public boolean has_following_source = true; public boolean has_following_source = true;
public transient UserKey[] after_filtered_source_keys; public transient UserKey[] after_filtered_source_keys;
public transient ParcelableLiteUser[] after_filtered_sources;
public ParcelableActivity() { public ParcelableActivity() {
} }

View File

@ -21,7 +21,11 @@ package org.mariotaku.microblog.library.twitter;
import org.mariotaku.microblog.library.MicroBlogException; import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.FavoritedPopup; import org.mariotaku.microblog.library.twitter.model.FavoritedPopup;
import org.mariotaku.microblog.library.twitter.model.StatusPage;
import org.mariotaku.restfu.annotation.method.GET; import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.param.Headers;
import org.mariotaku.restfu.annotation.param.KeyValue;
import org.mariotaku.restfu.annotation.param.Path;
import org.mariotaku.restfu.annotation.param.Query; import org.mariotaku.restfu.annotation.param.Query;
/** /**
@ -30,5 +34,14 @@ import org.mariotaku.restfu.annotation.param.Query;
public interface TwitterWeb { public interface TwitterWeb {
@GET("/i/activity/favorited_popup") @GET("/i/activity/favorited_popup")
@Headers(value = {@KeyValue(key = "Accept", value = "application/json")})
FavoritedPopup getFavoritedPopup(@Query("id") String statusId) throws MicroBlogException; FavoritedPopup getFavoritedPopup(@Query("id") String statusId) throws MicroBlogException;
@GET("/{screen_name}/status/{id}")
@Headers({@KeyValue(key = "Accept", value = "application/json, text/javascript, */*; q=0.01"),
@KeyValue(key = "X-Overlay-Request", value = "true"),
@KeyValue(key = "X-Requested-With", value = "XMLHttpRequest"),
@KeyValue(key = "User-Agent", value = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36")})
StatusPage getStatusPage(@Path("screen_name") String screenName, @Path("id") String statusId)
throws MicroBlogException;
} }

View File

@ -0,0 +1,36 @@
/*
* 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.microblog.library.twitter.model;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
/**
* Created by mariotaku on 2017/4/1.
*/
@JsonObject
public class StatusPage {
@JsonField(name = "page")
String page;
public String getPage() {
return page;
}
}

View File

@ -14,6 +14,7 @@ import android.webkit.URLUtil;
import org.mariotaku.microblog.library.MicroBlog; import org.mariotaku.microblog.library.MicroBlog;
import org.mariotaku.restfu.http.Endpoint; import org.mariotaku.restfu.http.Endpoint;
import org.mariotaku.restfu.http.MultiValueMap;
import org.mariotaku.restfu.http.SimpleValueMap; import org.mariotaku.restfu.http.SimpleValueMap;
import org.mariotaku.restfu.oauth.OAuthEndpoint; import org.mariotaku.restfu.oauth.OAuthEndpoint;
import org.mariotaku.restfu.oauth.OAuthToken; import org.mariotaku.restfu.oauth.OAuthToken;
@ -252,6 +253,6 @@ public class MicroBlogAPIFactory implements TwidereConstants {
public interface ExtraHeaders { public interface ExtraHeaders {
@NonNull @NonNull
List<Pair<String, String>> get(); List<Pair<String, String>> get(MultiValueMap<String> headers);
} }
} }

View File

@ -23,9 +23,9 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.database.CursorIndexOutOfBoundsException import android.database.CursorIndexOutOfBoundsException
import android.support.v4.util.LongSparseArray
import android.support.v4.widget.Space import android.support.v4.widget.Space
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -133,7 +133,7 @@ class ParcelableActivitiesAdapter(
private var filteredUserKeys: Array<UserKey>? = null private var filteredUserKeys: Array<UserKey>? = null
private val gapLoadingIds: MutableSet<ObjectId> = HashSet() private val gapLoadingIds: MutableSet<ObjectId> = HashSet()
private val reuseActivity = ParcelableActivity() private val reuseActivity = ParcelableActivity()
private val filterIdCache = LongSparseArray<Array<UserKey>?>() private val filterIdCache = SparseArray<Array<UserKey>?>()
init { init {
eventListener = EventListener(this) eventListener = EventListener(this)
@ -241,6 +241,8 @@ class ParcelableActivitiesAdapter(
} }
ITEM_VIEW_TYPE_TITLE_SUMMARY -> { ITEM_VIEW_TYPE_TITLE_SUMMARY -> {
val activity = getActivityInternal(position, raw = false, reuse = true) val activity = getActivityInternal(position, raw = false, reuse = true)
activity.after_filtered_source_keys = getAfterFilteredSourceKeys(position, false,
activity.source_keys)
(holder as ActivityTitleSummaryViewHolder).displayActivity(activity) (holder as ActivityTitleSummaryViewHolder).displayActivity(activity)
} }
ITEM_VIEW_TYPE_STUB -> { ITEM_VIEW_TYPE_STUB -> {
@ -274,12 +276,10 @@ class ParcelableActivitiesAdapter(
Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> { Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> {
if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY
filteredUserKeys?.let { filteredUserKeys?.let {
// ParcelableActivityUtils.initAfterFilteredSourceIds(activity, it, followingOnly) val afterFiltered = getAfterFilteredSourceKeys(position, false)
// filterIdCache[activity._id] = activity.after_filtered_source_ids if (afterFiltered != null && afterFiltered.isEmpty()) {
// val sourceIds = activity.after_filtered_source_keys return ITEM_VIEW_TYPE_EMPTY
// if (sourceIds != null && sourceIds.isEmpty()) { }
// return ITEM_VIEW_TYPE_EMPTY
// }
} }
return ITEM_VIEW_TYPE_TITLE_SUMMARY return ITEM_VIEW_TYPE_TITLE_SUMMARY
} }
@ -347,6 +347,15 @@ class ParcelableActivitiesAdapter(
}, defValue = -1L, raw = raw) }, 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>? { fun getData(): List<ParcelableActivity>? {
return data return data
} }
@ -356,7 +365,6 @@ class ParcelableActivitiesAdapter(
itemCounts[1] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0 itemCounts[1] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0
} }
private fun getActivityInternal(position: Int, raw: Boolean, reuse: Boolean): ParcelableActivity { private fun getActivityInternal(position: Int, raw: Boolean, reuse: Boolean): ParcelableActivity {
val dataPosition = position - activityStartIndex val dataPosition = position - activityStartIndex
val activityCount = getActivityCount(raw) val activityCount = getActivityCount(raw)
@ -375,6 +383,19 @@ class ParcelableActivitiesAdapter(
} }
} }
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 inline fun <T> getFieldValue(position: Int, private inline fun <T> getFieldValue(position: Int,
readCursorValueAction: (cursor: Cursor, indices: ObjectCursor.CursorIndices<ParcelableActivity>) -> T, readCursorValueAction: (cursor: Cursor, indices: ObjectCursor.CursorIndices<ParcelableActivity>) -> T,

View File

@ -0,0 +1,58 @@
/*
* 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.atto
import org.attoparser.dom.Document
import org.attoparser.dom.Element
/**
* Created by mariotaku on 2017/4/29.
*/
fun Document.firstElementOrNull(match: (Element) -> Boolean): Element? {
getChildrenOfType(Element::class.java).forEach { child ->
val element = child.firstElementOrNull(match)
if (element != null) return element
}
return null
}
fun Element.firstElementOrNull(match: (Element) -> Boolean): Element? {
if (match(this)) return this
getChildrenOfType(Element::class.java).forEach { child ->
val element = child.firstElementOrNull(match)
if (element != null) return element
}
return null
}
fun Element.filter(match: (Element) -> Boolean): List<Element> {
return filterTo(ArrayList(), match)
}
fun <C : MutableCollection<Element>> Element.filterTo(to: C, match: (Element) -> Boolean): C {
if (match(this)) {
to.add(this)
}
getChildrenOfType(Element::class.java).forEach { child ->
child.filterTo(to, match)
}
return to
}

View File

@ -0,0 +1,30 @@
/*
* 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.restfu
import org.mariotaku.restfu.http.MultiValueMap
/**
* Created by mariotaku on 2017/4/29.
*/
operator fun <T> MultiValueMap<T>.contains(key: String): Boolean {
return getFirst(key) != null
}

View File

@ -84,6 +84,7 @@ import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition
import org.mariotaku.twidere.adapter.iface.IStatusesAdapter import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.ProfileImageSize
import org.mariotaku.twidere.annotation.Referral import org.mariotaku.twidere.annotation.Referral
import org.mariotaku.twidere.constant.* import org.mariotaku.twidere.constant.*
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.* import org.mariotaku.twidere.constant.KeyboardShortcutConstants.*
@ -187,7 +188,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
} }
adapter.loadMoreSupportedPosition = supportedPositions adapter.loadMoreSupportedPosition = supportedPositions
setConversation(data) setConversation(data)
val canLoadAllReplies = loader.canLoadAllReplies() val canLoadAllReplies = loader.canLoadAllReplies
if (canLoadAllReplies) { if (canLoadAllReplies) {
adapter.setReplyError(null) adapter.setReplyError(null)
} else { } else {
@ -884,8 +885,8 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
nameView.updateText(formatter) nameView.updateText(formatter)
adapter.requestManager.loadProfileImage(context, status, adapter.profileImageStyle, adapter.requestManager.loadProfileImage(context, status, adapter.profileImageStyle,
itemView.profileImage.cornerRadius, itemView.profileImage.cornerRadiusRatio) itemView.profileImage.cornerRadius, itemView.profileImage.cornerRadiusRatio,
.into(itemView.profileImage) size = ProfileImageSize.ORIGINAL).into(itemView.profileImage)
val typeIconRes = Utils.getUserTypeIconRes(status.user_is_verified, status.user_is_protected) val typeIconRes = Utils.getUserTypeIconRes(status.user_is_verified, status.user_is_protected)
val typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.user_is_verified, status.user_is_protected) val typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.user_is_verified, status.user_is_protected)

View File

@ -22,16 +22,21 @@ package org.mariotaku.twidere.loader.statuses
import android.content.Context import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread import android.support.annotation.WorkerThread
import org.attoparser.config.ParseConfiguration
import org.attoparser.dom.DOMMarkupParser
import org.mariotaku.commons.parcel.ParcelUtils import org.mariotaku.commons.parcel.ParcelUtils
import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.TwitterWeb
import org.mariotaku.microblog.library.twitter.model.Paging import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.SearchQuery import org.mariotaku.microblog.library.twitter.model.SearchQuery
import org.mariotaku.microblog.library.twitter.model.Status import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.alias.MastodonStatus import org.mariotaku.twidere.alias.MastodonStatus
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.exception.APINotSupportedException import org.mariotaku.twidere.exception.APINotSupportedException
import org.mariotaku.twidere.extension.atto.filter
import org.mariotaku.twidere.extension.atto.firstElementOrNull
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.isOfficial import org.mariotaku.twidere.extension.model.isOfficial
@ -40,9 +45,11 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.pagination.PaginatedArrayList import org.mariotaku.twidere.model.pagination.PaginatedArrayList
import org.mariotaku.twidere.model.pagination.PaginatedList import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.model.pagination.Pagination
import org.mariotaku.twidere.model.pagination.SinceMaxPagination import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.model.util.ParcelableStatusUtils import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.util.InternalTwitterContentUtils import org.mariotaku.twidere.util.InternalTwitterContentUtils
import java.text.ParseException
import java.util.* import java.util.*
class ConversationLoader( class ConversationLoader(
@ -54,7 +61,8 @@ class ConversationLoader(
) : AbsRequestStatusesLoader(context, status.account_key, adapterData, null, -1, fromUser, loadingMore) { ) : AbsRequestStatusesLoader(context, status.account_key, adapterData, null, -1, fromUser, loadingMore) {
private val status = ParcelUtils.clone(status) private val status = ParcelUtils.clone(status)
private var canLoadAllReplies: Boolean = false var canLoadAllReplies: Boolean = false
private set
init { init {
ParcelableStatusUtils.makeOriginalStatus(this.status) ParcelableStatusUtils.makeOriginalStatus(this.status)
@ -66,9 +74,7 @@ class ConversationLoader(
AccountType.MASTODON -> return getMastodonStatuses(account, paging).mapTo(PaginatedArrayList()) { AccountType.MASTODON -> return getMastodonStatuses(account, paging).mapTo(PaginatedArrayList()) {
it.toParcelable(account) it.toParcelable(account)
} }
else -> return getMicroBlogStatuses(account, paging).mapMicroBlogToPaginated { else -> return getMicroBlogStatuses(account, paging)
it.toParcelable(account, profileImageSize)
}
} }
} }
@ -80,27 +86,33 @@ class ConversationLoader(
} }
@Throws(MicroBlogException::class) @Throws(MicroBlogException::class)
private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): List<Status> { private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): PaginatedList<ParcelableStatus> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java) val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
canLoadAllReplies = false canLoadAllReplies = false
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
val isOfficial = account.isOfficial(context) val isOfficial = account.isOfficial(context)
canLoadAllReplies = isOfficial canLoadAllReplies = isOfficial
if (isOfficial) { // if (isOfficial) {
return microBlog.showConversation(status.id, paging) // return microBlog.showConversation(status.id, paging).mapMicroBlogToPaginated {
} // it.toParcelable(account, profileImageSize)
// }
// }
return showConversationCompat(microBlog, account, status, true) return showConversationCompat(microBlog, account, status, true)
} }
AccountType.STATUSNET -> { AccountType.STATUSNET -> {
canLoadAllReplies = true canLoadAllReplies = true
status.extras?.statusnet_conversation_id?.let { status.extras?.statusnet_conversation_id?.let {
return microBlog.getStatusNetConversation(it, paging) return microBlog.getStatusNetConversation(it, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
} }
} }
AccountType.FANFOU -> { AccountType.FANFOU -> {
canLoadAllReplies = true canLoadAllReplies = true
return microBlog.getContextTimeline(status.id, paging) return microBlog.getContextTimeline(status.id, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
} }
else -> { else -> {
throw APINotSupportedException(account.type) throw APINotSupportedException(account.type)
@ -110,9 +122,14 @@ class ConversationLoader(
return showConversationCompat(microBlog, account, status, true) return showConversationCompat(microBlog, account, status, true)
} }
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, false)
}
@Throws(MicroBlogException::class) @Throws(MicroBlogException::class)
private fun showConversationCompat(twitter: MicroBlog, details: AccountDetails, private fun showConversationCompat(twitter: MicroBlog, details: AccountDetails,
status: ParcelableStatus, loadReplies: Boolean): List<Status> { status: ParcelableStatus, loadReplies: Boolean): PaginatedList<ParcelableStatus> {
val statuses = ArrayList<Status>() val statuses = ArrayList<Status>()
val pagination = this.pagination as? SinceMaxPagination val pagination = this.pagination as? SinceMaxPagination
val maxId = pagination?.maxId val maxId = pagination?.maxId
@ -120,6 +137,9 @@ class ConversationLoader(
val maxSortId = pagination?.maxSortId ?: -1 val maxSortId = pagination?.maxSortId ?: -1
val sinceSortId = pagination?.sinceSortId ?: -1 val sinceSortId = pagination?.sinceSortId ?: -1
val noSinceMaxId = maxId == null && sinceId == null val noSinceMaxId = maxId == null && sinceId == null
var nextPagination: Pagination? = null
// Load conversations // Load conversations
if (maxId != null && maxSortId < status.sort_id || noSinceMaxId) { if (maxId != null && maxSortId < status.sort_id || noSinceMaxId) {
var inReplyToId: String? = maxId ?: status.in_reply_to_status_id var inReplyToId: String? = maxId ?: status.in_reply_to_status_id
@ -131,35 +151,69 @@ class ConversationLoader(
count++ count++
} }
} }
if (loadReplies) { if (loadReplies || noSinceMaxId || sinceId != null && sinceSortId > status.sort_id) {
// Load replies // Load replies
if (sinceId != null && sinceSortId > status.sort_id || noSinceMaxId) { var repliesLoaded = false
val query = SearchQuery() try {
if (details.type == AccountType.TWITTER) { if (details.type == AccountType.TWITTER) {
query.query("to:${status.user_screen_name}") if (noSinceMaxId) {
statuses.addAll(loadTwitterWebReplies(details, twitter))
}
repliesLoaded = true
}
} catch (e: MicroBlogException) {
// Ignore
}
if (!repliesLoaded) {
val query = SearchQuery()
query.count(100)
if (details.type == AccountType.TWITTER) {
query.query("to:${status.user_screen_name} since_id:${status.id}")
} else { } else {
query.query("@${status.user_screen_name}") query.query("@${status.user_screen_name}")
} }
query.sinceId(sinceId ?: status.id) query.sinceId(sinceId ?: status.id)
try { try {
twitter.search(query).filterTo(statuses) { it.inReplyToStatusId == status.id } val queryResult = twitter.search(query)
val firstId = queryResult.firstOrNull()?.id
if (firstId != null) {
nextPagination = SinceMaxPagination.sinceId(firstId, 0)
}
queryResult.filterTo(statuses) { it.inReplyToStatusId == status.id }
} catch (e: MicroBlogException) { } catch (e: MicroBlogException) {
// Ignore for now // Ignore for now
} }
} }
} }
return statuses return statuses.mapTo(PaginatedArrayList()) {
it.toParcelable(details, profileImageSize)
}.apply {
this.nextPage = nextPagination
}
} }
fun canLoadAllReplies(): Boolean { private fun loadTwitterWebReplies(details: AccountDetails, twitter: MicroBlog): List<Status> {
return canLoadAllReplies val web = details.newMicroBlogInstance(context, TwitterWeb::class.java)
} val page = web.getStatusPage(status.user_screen_name, status.id).page
@WorkerThread val parser = DOMMarkupParser(ParseConfiguration.htmlConfiguration())
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean { val statusIds = ArrayList<String>()
return InternalTwitterContentUtils.isFiltered(database, status, false)
}
try {
val document = parser.parse(page)
val repliesElement = document.firstElementOrNull { element ->
element.getAttributeValue("data-component-context") == "replies"
} ?: throw MicroBlogException("No replies data found")
repliesElement.filter {
it.getAttributeValue("data-item-type") == "tweet" && it.hasAttribute("data-item-id")
}.mapTo(statusIds) { it.getAttributeValue("data-item-id") }
} catch (e: ParseException) {
throw MicroBlogException(e)
}
if (statusIds.isEmpty()) {
throw MicroBlogException("Invalid response")
}
return twitter.lookupStatuses(statusIds.distinct().toTypedArray())
}
} }

View File

@ -45,15 +45,12 @@ object ParcelableActivityUtils {
} }
fun getAfterFilteredSources(activity: ParcelableActivity): Array<ParcelableLiteUser> { fun getAfterFilteredSources(activity: ParcelableActivity): Array<ParcelableLiteUser> {
if (activity.after_filtered_sources != null) return activity.after_filtered_sources val sources = activity.sources_lite ?: return emptyArray()
if (activity.after_filtered_source_keys == null || activity.sources.size == activity.after_filtered_source_keys.size) { val afterFilteredKeys = activity.after_filtered_source_keys?.takeIf {
return activity.sources_lite it.size != sources.size
} } ?: return sources
val result = Array(activity.after_filtered_source_keys.size) { idx -> return sources.filter { it.key in afterFilteredKeys }.toTypedArray()
return@Array activity.sources_lite.find { it.key == activity.after_filtered_source_keys[idx] }!!
}
activity.after_filtered_sources = result
return result
} }

View File

@ -35,14 +35,18 @@ import org.mariotaku.ktextension.useCursor
import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Activity import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.sqliteqb.library.* import org.mariotaku.sqliteqb.library.*
import org.mariotaku.sqliteqb.library.Columns.Column import org.mariotaku.sqliteqb.library.Columns.Column
import org.mariotaku.sqliteqb.library.query.SQLSelectQuery import org.mariotaku.sqliteqb.library.query.SQLSelectQuery
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.* import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.constant.IntentConstants import org.mariotaku.twidere.constant.IntentConstants
import org.mariotaku.twidere.constant.databaseItemLimitKey import org.mariotaku.twidere.constant.databaseItemLimitKey
import org.mariotaku.twidere.extension.model.* import org.mariotaku.twidere.extension.model.*
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.rawQuery import org.mariotaku.twidere.extension.rawQuery
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
@ -919,16 +923,24 @@ object DataStoreUtils {
@Throws(MicroBlogException::class) @Throws(MicroBlogException::class)
fun findStatus(context: Context, accountKey: UserKey, statusId: String): ParcelableStatus { fun findStatus(context: Context, accountKey: UserKey, statusId: String): ParcelableStatus {
val cached = findStatusInDatabases(context, accountKey, statusId) val cached = findStatusInDatabases(context, accountKey, statusId)
val profileImageSize = context.getString(R.string.profile_image_size)
if (cached != null) return cached if (cached != null) return cached
val details = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, val details = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey,
true) ?: throw MicroBlogException("No account") true) ?: throw MicroBlogException("No account")
val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java) val status = when (details.type) {
val result = microBlog.showStatus(statusId) AccountType.MASTODON -> {
val mastodon = details.newMicroBlogInstance(context, Mastodon::class.java)
mastodon.fetchStatus(statusId).toParcelable(details)
}
else -> {
val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java)
microBlog.showStatus(statusId).toParcelable(details, profileImageSize)
}
}
val where = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY), val where = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.ID)).sql Expression.equalsArgs(Statuses.ID)).sql
val whereArgs = arrayOf(accountKey.toString(), statusId) val whereArgs = arrayOf(accountKey.toString(), statusId)
val resolver = context.contentResolver val resolver = context.contentResolver
val status = result.toParcelable(details)
resolver.delete(CachedStatuses.CONTENT_URI, where, whereArgs) resolver.delete(CachedStatuses.CONTENT_URI, where, whereArgs)
resolver.insert(CachedStatuses.CONTENT_URI, ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java).create(status)) resolver.insert(CachedStatuses.CONTENT_URI, ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java).create(status))
return status return status

View File

@ -49,8 +49,8 @@ class TwidereHttpRequestFactory(
headers.add("Authorization", RestFuUtils.sanitizeHeader(authorization.getHeader(endpoint, info))) headers.add("Authorization", RestFuUtils.sanitizeHeader(authorization.getHeader(endpoint, info)))
} }
if (extraHeaders != null) { if (extraHeaders != null) {
for (pair in extraHeaders.get()) { for ((first, second) in extraHeaders.get(info.headers)) {
headers.add(pair.first, RestFuUtils.sanitizeHeader(pair.second)) headers.add(first, RestFuUtils.sanitizeHeader(second))
} }
} }
return HttpRequest(restMethod, url, headers, info.getBody(converterFactory), null) return HttpRequest(restMethod, url, headers, info.getBody(converterFactory), null)

View File

@ -21,6 +21,8 @@ package org.mariotaku.twidere.util.api
import android.os.Build import android.os.Build
import org.mariotaku.ktextension.bcp47Tag import org.mariotaku.ktextension.bcp47Tag
import org.mariotaku.restfu.http.MultiValueMap
import org.mariotaku.twidere.extension.restfu.contains
import org.mariotaku.twidere.util.MicroBlogAPIFactory.ExtraHeaders import org.mariotaku.twidere.util.MicroBlogAPIFactory.ExtraHeaders
import java.util.* import java.util.*
@ -34,10 +36,12 @@ object TwitterAndroidExtraHeaders : ExtraHeaders {
const val apiVersion = "5" const val apiVersion = "5"
const val internalVersionName = "7160062-r-930" const val internalVersionName = "7160062-r-930"
override fun get(): List<Pair<String, String>> { override fun get(headers: MultiValueMap<String>): List<Pair<String, String>> {
val result = ArrayList<Pair<String, String>>() val result = ArrayList<Pair<String, String>>()
val language = Locale.getDefault().bcp47Tag val language = Locale.getDefault().bcp47Tag
result.add(Pair("User-Agent", userAgent)) if ("User-Agent" !in headers) {
result.add(Pair("User-Agent", userAgent))
}
result.add(Pair("Accept-Language", language)) result.add(Pair("Accept-Language", language))
result.add(Pair("X-Twitter-Client", clientName)) result.add(Pair("X-Twitter-Client", clientName))
result.add(Pair("X-Twitter-Client-Language", language)) result.add(Pair("X-Twitter-Client-Language", language))

View File

@ -21,6 +21,8 @@ package org.mariotaku.twidere.util.api
import android.os.Build import android.os.Build
import org.mariotaku.ktextension.bcp47Tag import org.mariotaku.ktextension.bcp47Tag
import org.mariotaku.restfu.http.MultiValueMap
import org.mariotaku.twidere.extension.restfu.contains
import org.mariotaku.twidere.util.MicroBlogAPIFactory.ExtraHeaders import org.mariotaku.twidere.util.MicroBlogAPIFactory.ExtraHeaders
import java.util.* import java.util.*
@ -36,10 +38,12 @@ object TwitterMacExtraHeaders : ExtraHeaders {
const val platformArchitecture = "x86_64" const val platformArchitecture = "x86_64"
const val internalVersionName = "5002734" const val internalVersionName = "5002734"
override fun get(): List<Pair<String, String>> { override fun get(headers: MultiValueMap<String>): List<Pair<String, String>> {
val result = ArrayList<Pair<String, String>>() val result = ArrayList<Pair<String, String>>()
val language = Locale.getDefault().bcp47Tag val language = Locale.getDefault().bcp47Tag
result.add(Pair("User-Agent", userAgent)) if ("User-Agent" !in headers) {
result.add(Pair("User-Agent", userAgent))
}
result.add(Pair("Accept-Language", language)) result.add(Pair("Accept-Language", language))
result.add(Pair("X-Twitter-Client", clientName)) result.add(Pair("X-Twitter-Client", clientName))
result.add(Pair("X-Twitter-Client-Version", versionName)) result.add(Pair("X-Twitter-Client-Version", versionName))

View File

@ -19,6 +19,8 @@
package org.mariotaku.twidere.util.api package org.mariotaku.twidere.util.api
import org.mariotaku.restfu.http.MultiValueMap
import org.mariotaku.twidere.extension.restfu.contains
import org.mariotaku.twidere.util.MicroBlogAPIFactory import org.mariotaku.twidere.util.MicroBlogAPIFactory
/** /**
@ -26,8 +28,8 @@ import org.mariotaku.twidere.util.MicroBlogAPIFactory
*/ */
class UserAgentExtraHeaders(val userAgent: String?) : MicroBlogAPIFactory.ExtraHeaders { class UserAgentExtraHeaders(val userAgent: String?) : MicroBlogAPIFactory.ExtraHeaders {
override fun get(): List<Pair<String, String>> { override fun get(headers: MultiValueMap<String>): List<Pair<String, String>> {
if (userAgent == null) return emptyList() if (userAgent == null || "User-Agent" in headers) return emptyList()
return listOf(Pair("User-Agent", userAgent)) return listOf(Pair("User-Agent", userAgent))
} }
} }