added timeline filter

supporting new reply style
This commit is contained in:
Mariotaku Lee 2017-03-31 16:05:50 +08:00
parent 31543f1d25
commit 4ca81cac7d
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
24 changed files with 470 additions and 83 deletions

View File

@ -57,7 +57,8 @@ subprojects {
MediaViewerLibrary: '0.9.23',
MultiValueSwitch : '0.9.8',
PickNCrop : '0.9.22',
AndroidGIFDrawable: '1.2.6'
AndroidGIFDrawable: '1.2.6',
KPreferences : '0.9.6'
]
}

View File

@ -30,11 +30,6 @@ public class StatusUpdate extends SimpleValueMap {
put("status", status);
}
public StatusUpdate displayCoordinates(final boolean displayCoordinates) {
setDisplayCoordinates(displayCoordinates);
return this;
}
public void setInReplyToStatusId(final String inReplyToStatusId) {
put("in_reply_to_status_id", inReplyToStatusId);
}
@ -43,14 +38,6 @@ public class StatusUpdate extends SimpleValueMap {
put("repost_status_id", repostStatusId);
}
public void setLocation(final GeoLocation location) {
remove("lat");
remove("long");
if (location == null) return;
put("lat", location.getLatitude());
put("long", location.getLongitude());
}
public void setMediaIds(final String... mediaIds) {
remove("media_ids");
if (mediaIds == null) return;
@ -71,17 +58,22 @@ public class StatusUpdate extends SimpleValueMap {
}
public void setDisplayCoordinates(final boolean displayCoordinates) {
public StatusUpdate displayCoordinates(final boolean displayCoordinates) {
put("display_coordinates", displayCoordinates);
return this;
}
public void setPossiblySensitive(final boolean possiblySensitive) {
put("possibly_sensitive", possiblySensitive);
public StatusUpdate autoPopulateReplyMetadata(final boolean autoPopulateReplyMetadata) {
put("auto_populate_reply_metadata", autoPopulateReplyMetadata);
return this;
}
public StatusUpdate location(final GeoLocation location) {
setLocation(location);
remove("lat");
remove("long");
if (location == null) return this;
put("lat", location.getLatitude());
put("long", location.getLongitude());
return this;
}
@ -101,7 +93,7 @@ public class StatusUpdate extends SimpleValueMap {
}
public StatusUpdate possiblySensitive(final boolean possiblySensitive) {
setPossiblySensitive(possiblySensitive);
put("possibly_sensitive", possiblySensitive);
return this;
}

View File

@ -185,7 +185,7 @@ dependencies {
compile "com.github.mariotaku.CommonsLibrary:io:${libVersions['MariotakuCommons']}"
compile "com.github.mariotaku.CommonsLibrary:text:${libVersions['MariotakuCommons']}"
compile "com.github.mariotaku.CommonsLibrary:text-kotlin:${libVersions['MariotakuCommons']}"
compile 'com.github.mariotaku:KPreferences:0.9.5'
compile "com.github.mariotaku:KPreferences:${libVersions['KPreferences']}"
compile 'com.github.mariotaku:Chameleon:0.9.16'
compile "org.jetbrains.kotlin:kotlin-stdlib:${libVersions['Kotlin']}"

View File

@ -33,6 +33,9 @@ public class DefaultFeatures {
@JsonField(name = "media_link_counts_in_status")
boolean mediaLinkCountsInStatus = false;
@JsonField(name = "mentions_counts_in_status")
boolean mentionsCountsInStatus = false;
@JsonField(name = "default_twitter_consumer_key")
String defaultTwitterConsumerKey;
@ -49,6 +52,10 @@ public class DefaultFeatures {
return mediaLinkCountsInStatus;
}
public boolean isMentionsCountsInStatus() {
return mentionsCountsInStatus;
}
public String getDefaultTwitterConsumerKey() {
return defaultTwitterConsumerKey;
}

View File

@ -0,0 +1,31 @@
/*
* 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.model.timeline;
import android.content.Context;
import android.os.Parcelable;
/**
* Created by mariotaku on 2017/3/31.
*/
public interface TimelineFilter extends Parcelable {
CharSequence getSummary(Context context);
}

View File

@ -0,0 +1,88 @@
/*
* 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.model.timeline;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease;
import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease;
import org.mariotaku.twidere.R;
/**
* Created by mariotaku on 2017/3/31.
*/
@ParcelablePlease
public class UserTimelineFilter implements TimelineFilter, Parcelable {
@ParcelableThisPlease
boolean includeRetweets = true;
@ParcelableThisPlease
boolean includeReplies = true;
public boolean isIncludeRetweets() {
return includeRetweets;
}
public void setIncludeRetweets(final boolean includeRetweets) {
this.includeRetweets = includeRetweets;
}
public boolean isIncludeReplies() {
return includeReplies;
}
public void setIncludeReplies(final boolean includeReplies) {
this.includeReplies = includeReplies;
}
@Override
public CharSequence getSummary(final Context context) {
if (includeRetweets && includeReplies) {
return context.getString(R.string.label_statuses_retweets_replies);
} else if (includeReplies) {
return context.getString(R.string.label_statuses_replies);
}
return context.getString(R.string.label_statuses);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
UserTimelineFilterParcelablePlease.writeToParcel(this, dest, flags);
}
public static final Creator<UserTimelineFilter> CREATOR = new Creator<UserTimelineFilter>() {
public UserTimelineFilter createFromParcel(Parcel source) {
UserTimelineFilter target = new UserTimelineFilter();
UserTimelineFilterParcelablePlease.readFromParcel(target, source);
return target;
}
public UserTimelineFilter[] newArray(int size) {
return new UserTimelineFilter[size];
}
};
}

View File

@ -1434,7 +1434,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
val accountKeys = accountsAdapter.selectedAccountKeys
val accounts = AccountUtils.getAllAccountDetails(AccountManager.get(this), accountKeys, true)
val ignoreMentions = accounts.all { it.type == AccountType.TWITTER }
val tweetLength = validator.getTweetLength(text, ignoreMentions)
val tweetLength = validator.getTweetLength(text, ignoreMentions &&
defaultFeatures.isMentionsCountsInStatus)
val maxLength = statusTextCount.maxLength
if (accountsAdapter.isSelectionEmpty) {
editText.error = getString(R.string.message_toast_no_account_selected)
@ -1491,10 +1492,10 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
private fun updateTextCount() {
val am = AccountManager.get(this)
val text = editText.text?.toString() ?: return
val ignoreMentions = accountsAdapter.selectedAccountKeys.all {
val ignoreMentions = inReplyToStatus != null && accountsAdapter.selectedAccountKeys.all {
val account = AccountUtils.findByAccountKey(am, it) ?: return@all false
return@all account.getAccountType(am) == AccountType.TWITTER
}
} && defaultFeatures.isMentionsCountsInStatus
statusTextCount.textCount = validator.getTweetLength(text, ignoreMentions)
}

View File

@ -40,13 +40,19 @@ import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.annotation.PreviewStyle
import org.mariotaku.twidere.constant.*
import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_DISPLAY_SENSITIVE_CONTENTS
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.ItemCounts
import org.mariotaku.twidere.model.ObjectId
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.timeline.TimelineFilter
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.util.StatusAdapterLinkClickHandler
import org.mariotaku.twidere.util.TwidereLinkify
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.view.holder.EmptyViewHolder
import org.mariotaku.twidere.view.holder.GapViewHolder
import org.mariotaku.twidere.view.holder.LoadIndicatorViewHolder
import org.mariotaku.twidere.view.holder.TimelineFilterHeaderViewHolder
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
import java.util.*
@ -56,8 +62,8 @@ import java.util.*
abstract class ParcelableStatusesAdapter(
context: Context,
requestManager: RequestManager
) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context, requestManager), IStatusesAdapter<List<ParcelableStatus>>,
IItemCountsAdapter {
) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context, requestManager),
IStatusesAdapter<List<ParcelableStatus>>, IItemCountsAdapter {
protected val inflater: LayoutInflater = LayoutInflater.from(context)
@ -105,6 +111,12 @@ abstract class ParcelableStatusesAdapter(
notifyDataSetChanged()
}
var timelineFilter: TimelineFilter? = null
set(value) {
field = value
notifyDataSetChanged()
}
private var data: List<ParcelableStatus>? = null
private var displayPositions: IntArray? = null
private var displayDataCount: Int = 0
@ -112,7 +124,7 @@ abstract class ParcelableStatusesAdapter(
private var showingActionCardId = RecyclerView.NO_ID
override val itemCounts = ItemCounts(4)
override val itemCounts = ItemCounts(5)
val statusStartIndex: Int
get() = getItemStartPosition(ITEM_INDEX_STATUS)
@ -148,8 +160,8 @@ abstract class ParcelableStatusesAdapter(
if (data is ObjectCursor) {
val cursor = (data as ObjectCursor).cursor
if (!cursor.moveToPosition(dataPosition)) return false
val indices = (data as ObjectCursor).indices as ParcelableStatusCursorIndices
return cursor.getShort(indices.is_gap).toInt() == 1
val indices = (data as ObjectCursor).indices
return cursor.getShort(indices[Statuses.STATUS_ID]).toInt() == 1
}
return getStatus(position).is_gap
}
@ -205,8 +217,8 @@ abstract class ParcelableStatusesAdapter(
return mask + status.hashCode()
}
ITEM_INDEX_STATUS -> return getFieldValue(position, { cursor, indices ->
val accountKey = UserKey.valueOf(cursor.getString(indices.account_key))
val id = cursor.getString(indices.id)
val accountKey = UserKey.valueOf(cursor.getString(indices[Statuses.ACCOUNT_KEY]))
val id = cursor.getString(indices[Statuses.STATUS_ID])
return@getFieldValue ParcelableStatus.calculateHashCode(accountKey, id).toLong()
}, { status ->
return@getFieldValue status.hashCode().toLong()
@ -217,7 +229,7 @@ abstract class ParcelableStatusesAdapter(
override fun getStatusId(position: Int, raw: Boolean): String {
return getFieldValue(position, { cursor, indices ->
return@getFieldValue cursor.getString(indices.id)
return@getFieldValue cursor.getString(indices[Statuses.STATUS_ID])
}, { status ->
return@getFieldValue status.id
}, "")
@ -225,7 +237,7 @@ abstract class ParcelableStatusesAdapter(
fun getStatusSortId(position: Int, raw: Boolean): Long {
return getFieldValue(position, { cursor, indices ->
return@getFieldValue cursor.safeGetLong(indices.sort_id)
return@getFieldValue cursor.safeGetLong(indices[Statuses.SORT_ID])
}, { status ->
return@getFieldValue status.sort_id
}, -1L, raw)
@ -233,7 +245,7 @@ abstract class ParcelableStatusesAdapter(
override fun getStatusTimestamp(position: Int, raw: Boolean): Long {
return getFieldValue(position, { cursor, indices ->
return@getFieldValue cursor.safeGetLong(indices.timestamp)
return@getFieldValue cursor.safeGetLong(indices[Statuses.STATUS_TIMESTAMP])
}, { status ->
return@getFieldValue status.timestamp
}, -1L)
@ -241,9 +253,9 @@ abstract class ParcelableStatusesAdapter(
override fun getStatusPositionKey(position: Int, raw: Boolean): Long {
return getFieldValue(position, { cursor, indices ->
val positionKey = cursor.safeGetLong(indices.position_key)
val positionKey = cursor.safeGetLong(indices[Statuses.POSITION_KEY])
if (positionKey > 0) return@getFieldValue positionKey
return@getFieldValue cursor.safeGetLong(indices.timestamp)
return@getFieldValue cursor.safeGetLong(indices[Statuses.STATUS_TIMESTAMP])
}, { status ->
val positionKey = status.position_key
if (positionKey > 0) return@getFieldValue positionKey
@ -254,7 +266,7 @@ abstract class ParcelableStatusesAdapter(
override fun getAccountKey(position: Int, raw: Boolean): UserKey {
val def: UserKey? = null
return getFieldValue(position, { cursor, indices ->
return@getFieldValue UserKey.valueOf(cursor.getString(indices.account_key))
return@getFieldValue UserKey.valueOf(cursor.getString(indices[Statuses.ACCOUNT_KEY]))
}, { status ->
return@getFieldValue status.account_key
}, def, raw)!!
@ -281,9 +293,6 @@ abstract class ParcelableStatusesAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
VIEW_TYPE_STATUS -> {
return onCreateStatusViewHolder(parent) as RecyclerView.ViewHolder
}
ITEM_VIEW_TYPE_GAP -> {
val view = inflater.inflate(GapViewHolder.layoutResource, parent, false)
return GapViewHolder(this, view)
@ -292,9 +301,17 @@ abstract class ParcelableStatusesAdapter(
val view = inflater.inflate(R.layout.list_item_load_indicator, parent, false)
return LoadIndicatorViewHolder(view)
}
VIEW_TYPE_STATUS -> {
return onCreateStatusViewHolder(parent) as RecyclerView.ViewHolder
}
VIEW_TYPE_EMPTY -> {
return EmptyViewHolder(Space(context))
}
VIEW_TYPE_FILTER_HEADER -> {
val view = inflater.inflate(TimelineFilterHeaderViewHolder.layoutResource,
parent, false)
return TimelineFilterHeaderViewHolder(this, view)
}
}
throw IllegalStateException("Unknown view type " + viewType)
}
@ -307,6 +324,9 @@ abstract class ParcelableStatusesAdapter(
(holder as IStatusViewHolder).displayStatus(status, displayInReplyTo = isShowInReplyTo,
displayPinned = countIdx == ITEM_INDEX_PINNED_STATUS)
}
VIEW_TYPE_FILTER_HEADER -> {
(holder as TimelineFilterHeaderViewHolder).display(timelineFilter!!)
}
ITEM_VIEW_TYPE_GAP -> {
val status = getStatus(position)
val loading = gapLoadingIds.any { it.accountKey == status.account_key && it.id == status.id }
@ -333,6 +353,9 @@ abstract class ParcelableStatusesAdapter(
return VIEW_TYPE_STATUS
}
}
ITEM_INDEX_FILTER_HEADER -> {
return VIEW_TYPE_FILTER_HEADER
}
}
throw AssertionError()
}
@ -404,7 +427,7 @@ abstract class ParcelableStatusesAdapter(
}
private inline fun <T> getFieldValue(position: Int,
readCursorValueAction: (cursor: Cursor, indices: ParcelableStatusCursorIndices) -> T,
readCursorValueAction: (cursor: Cursor, indices: ObjectCursor.CursorIndices<ParcelableStatus>) -> T,
readStatusValueAction: (status: ParcelableStatus) -> T,
defValue: T, raw: Boolean = false): T {
if (data is ObjectCursor) {
@ -414,7 +437,7 @@ abstract class ParcelableStatusesAdapter(
}
val cursor = (data as ObjectCursor).cursor
if (!cursor.safeMoveToPosition(dataPosition)) return defValue
val indices = (data as ObjectCursor).indices as ParcelableStatusCursorIndices
val indices = (data as ObjectCursor).indices
return readCursorValueAction(cursor, indices)
}
return readStatusValueAction(getStatus(position, raw))
@ -443,6 +466,7 @@ abstract class ParcelableStatusesAdapter(
private fun updateItemCount() {
itemCounts[ITEM_INDEX_LOAD_START_INDICATOR] = if (ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition) 1 else 0
itemCounts[ITEM_INDEX_FILTER_HEADER] = if (timelineFilter != null) 1 else 0
itemCounts[ITEM_INDEX_PINNED_STATUS] = pinnedStatuses?.size ?: 0
itemCounts[ITEM_INDEX_STATUS] = getStatusCount(false)
itemCounts[ITEM_INDEX_LOAD_END_INDICATOR] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0
@ -451,11 +475,13 @@ abstract class ParcelableStatusesAdapter(
companion object {
const val VIEW_TYPE_STATUS = 2
const val VIEW_TYPE_EMPTY = 3
const val VIEW_TYPE_FILTER_HEADER = 4
const val ITEM_INDEX_LOAD_START_INDICATOR = 0
const val ITEM_INDEX_PINNED_STATUS = 1
const val ITEM_INDEX_STATUS = 2
const val ITEM_INDEX_LOAD_END_INDICATOR = 3
const val ITEM_INDEX_FILTER_HEADER = 1
const val ITEM_INDEX_PINNED_STATUS = 2
const val ITEM_INDEX_STATUS = 3
const val ITEM_INDEX_LOAD_END_INDICATOR = 4
}

View File

@ -2,6 +2,7 @@ package org.mariotaku.twidere.constant
import android.content.SharedPreferences
import android.os.Build
import android.support.v4.util.ArraySet
import android.text.TextUtils
import org.apache.commons.lang3.LocaleUtils
import org.mariotaku.kpreferences.*
@ -18,6 +19,7 @@ import org.mariotaku.twidere.model.CustomAPIConfig
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.account.cred.Credentials
import org.mariotaku.twidere.model.sync.SyncProviderInfo
import org.mariotaku.twidere.model.timeline.UserTimelineFilter
import org.mariotaku.twidere.preference.ThemeBackgroundPreference
import org.mariotaku.twidere.util.sync.SyncProviderInfoFactory
import java.util.*
@ -242,3 +244,28 @@ object composeAccountsKey : KSimpleKey<Array<UserKey>?>(KEY_COMPOSE_ACCOUNTS, nu
}
}
object userTimelineFilterKey : KSimpleKey<UserTimelineFilter>("user_timeline_filter", UserTimelineFilter()) {
override fun read(preferences: SharedPreferences): UserTimelineFilter {
val rawString = preferences.getString(key, null) ?: return def
val options = rawString.split(",")
return UserTimelineFilter().apply {
isIncludeReplies = "replies" in options
isIncludeRetweets = "retweets" in options
}
}
override fun write(editor: SharedPreferences.Editor, value: UserTimelineFilter): Boolean {
val options = ArraySet<String>().apply {
if (value.isIncludeReplies) {
add("replies")
}
if (value.isIncludeRetweets) {
add("retweets")
}
}.joinToString(",")
editor.putString(key, options)
return true
}
}

View File

@ -57,6 +57,7 @@ import org.mariotaku.twidere.loader.iface.IExtendedLoader
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.analyzer.Share
import org.mariotaku.twidere.model.event.StatusListChangedEvent
import org.mariotaku.twidere.model.timeline.TimelineFilter
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.util.*
@ -127,6 +128,10 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
get() = (parentFragment as? StatusesFragmentDelegate)?.shouldInitLoader ?: true
protected open val enableTimelineFilter: Boolean = false
protected open val timelineFilter: TimelineFilter? = null
// Fragment life cycles
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
@ -311,6 +316,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
}
// 2. Change adapter data
adapterData = data
adapter.timelineFilter = timelineFilter
refreshEnabled = true
@ -490,7 +496,8 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
protected fun saveReadPosition(position: Int) {
if (host == null) return
if (position == RecyclerView.NO_POSITION || adapter.getStatusCount(false) <= 0) return
val status = adapter.getStatus(position)
val status = adapter.getStatus(position.coerceIn(rangeOfSize(adapter.statusStartIndex,
adapter.getStatusCount(false))))
val readPosition = if (useSortIdAsReadPosition) {
status.sort_id
} else {

View File

@ -49,7 +49,6 @@ import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_QUICK_SEND
import org.mariotaku.twidere.extension.applyTheme
import org.mariotaku.twidere.extension.getTweetLength
import org.mariotaku.twidere.extension.model.getAccountType
import org.mariotaku.twidere.extension.model.textLimit
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.util.AccountUtils
@ -200,9 +199,7 @@ class RetweetQuoteDialogFragment : BaseDialogFragment() {
}
val textCountView = dialog.findViewById(R.id.commentTextCount) as StatusTextCountView
val am = AccountManager.get(context)
val ignoreMentions = AccountUtils.findByAccountKey(am, accountKey)?.getAccountType(am) ==
AccountType.TWITTER
textCountView.textCount = validator.getTweetLength(s.toString(), ignoreMentions)
textCountView.textCount = validator.getTweetLength(s.toString(), false)
}
private val status: ParcelableStatus

View File

@ -1425,22 +1425,12 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
tabArgs.putParcelable(EXTRA_USER_KEY, args.getParcelable<Parcelable>(EXTRA_USER_KEY))
tabArgs.putString(EXTRA_SCREEN_NAME, args.getString(EXTRA_SCREEN_NAME))
}
if (userKey?.host == USER_TYPE_TWITTER_COM) {
pagerAdapter.add(cls = UserTimelineFragment::class.java, args = Bundle(tabArgs).apply {
putBoolean(EXTRA_EXCLUDE_REPLIES, true)
this[UserTimelineFragment.EXTRA_ENABLE_TIMELINE_FILTER] = true
}, name = getString(R.string.title_statuses), type = TAB_TYPE_STATUSES,
position = TAB_POSITION_STATUSES)
pagerAdapter.add(cls = UserTimelineFragment::class.java, args = tabArgs,
name = getString(R.string.title_statuses_and_replies), type = TAB_TYPE_STATUSES_WITH_REPLIES,
position = TAB_POSITION_STATUSES)
} else {
pagerAdapter.add(cls = UserTimelineFragment::class.java, args = tabArgs,
name = getString(R.string.title_statuses), type = TAB_TYPE_STATUSES,
position = TAB_POSITION_STATUSES)
}
pagerAdapter.add(cls = UserMediaTimelineFragment::class.java, args = tabArgs,
name = getString(R.string.media), type = TAB_TYPE_MEDIA,
position = TAB_POSITION_MEDIA)
name = getString(R.string.media), type = TAB_TYPE_MEDIA, position = TAB_POSITION_MEDIA)
if (preferences.getBoolean(KEY_I_WANT_MY_STARS_BACK)) {
pagerAdapter.add(cls = UserFavoritesFragment::class.java, args = tabArgs,
name = getString(R.string.title_favorites), type = TAB_TYPE_FAVORITES,

View File

@ -9,9 +9,11 @@ import android.support.v7.widget.RecyclerView
import android.support.v7.widget.StaggeredGridLayoutManager
import android.text.TextUtils
import com.bumptech.glide.Glide
import org.mariotaku.kpreferences.get
import org.mariotaku.twidere.adapter.StaggeredGridParcelableStatusesAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.constant.userTimelineFilterKey
import org.mariotaku.twidere.extensions.reachingEnd
import org.mariotaku.twidere.extensions.reachingStart
import org.mariotaku.twidere.loader.MediaTimelineLoader

View File

@ -19,15 +19,25 @@
package org.mariotaku.twidere.fragment
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.v4.content.Loader
import android.support.v7.app.AlertDialog
import edu.tsinghua.hotmobi.model.TimelineType
import org.mariotaku.kpreferences.get
import org.mariotaku.kpreferences.set
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.constant.userTimelineFilterKey
import org.mariotaku.twidere.extension.applyTheme
import org.mariotaku.twidere.loader.UserTimelineLoader
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.timeline.TimelineFilter
import org.mariotaku.twidere.model.timeline.UserTimelineFilter
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.view.holder.TimelineFilterHeaderViewHolder
import java.util.*
/**
@ -42,11 +52,9 @@ class UserTimelineFragment : ParcelableStatusesFragment() {
override val savedStatusesFileArgs: Array<String>?
get() {
val args = arguments!!
val accountKey = Utils.getAccountKey(context, args)!!
val userKey = args.getParcelable<UserKey>(EXTRA_USER_KEY)
val screenName = args.getString(EXTRA_SCREEN_NAME)
val excludeReplies = args.getBoolean(EXTRA_EXCLUDE_REPLIES)
val accountKey = Utils.getAccountKey(context, arguments)!!
val userKey = arguments.getParcelable<UserKey>(EXTRA_USER_KEY)
val screenName = arguments.getString(EXTRA_SCREEN_NAME)
val result = ArrayList<String>()
result.add(AUTHORITY_USER_TIMELINE)
result.add("account=$accountKey")
@ -57,21 +65,24 @@ class UserTimelineFragment : ParcelableStatusesFragment() {
} else {
return null
}
if (excludeReplies) {
result.add("exclude_replies")
(timelineFilter as? UserTimelineFilter)?.let {
if (it.isIncludeReplies) {
result.add("include_replies")
}
if (it.isIncludeRetweets) {
result.add("include_retweets")
}
}
return result.toTypedArray()
}
override val readPositionTagWithArguments: String?
get() {
val args = arguments!!
val tabPosition = args.getInt(EXTRA_TAB_POSITION, -1)
if (arguments.getLong(EXTRA_TAB_ID, -1) < 0) return null
val sb = StringBuilder("user_timeline_")
if (tabPosition < 0) return null
val userKey = args.getParcelable<UserKey>(EXTRA_USER_KEY)
val screenName = args.getString(EXTRA_SCREEN_NAME)
val userKey = arguments.getParcelable<UserKey>(EXTRA_USER_KEY)
val screenName = arguments.getString(EXTRA_SCREEN_NAME)
if (userKey != null) {
sb.append(userKey)
} else if (screenName != null) {
@ -82,6 +93,12 @@ class UserTimelineFragment : ParcelableStatusesFragment() {
return sb.toString()
}
override val enableTimelineFilter: Boolean
get() = arguments.getBoolean(EXTRA_ENABLE_TIMELINE_FILTER)
override val timelineFilter: TimelineFilter?
get() = if (enableTimelineFilter) preferences[userTimelineFilterKey] else null
override fun onCreateStatusesLoader(context: Context, args: Bundle, fromUser: Boolean):
Loader<List<ParcelableStatus>?> {
refreshing = true
@ -93,10 +110,10 @@ class UserTimelineFragment : ParcelableStatusesFragment() {
val screenName = args.getString(EXTRA_SCREEN_NAME)
val tabPosition = args.getInt(EXTRA_TAB_POSITION, -1)
val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false)
val excludeReplies = args.getBoolean(EXTRA_EXCLUDE_REPLIES, false)
val pinnedIds = if (adapter.hasPinnedStatuses) null else pinnedStatusIds
return UserTimelineLoader(context, accountKey, userKey, screenName, sinceId, maxId, data,
savedStatusesFileArgs, tabPosition, fromUser, loadingMore, pinnedIds, excludeReplies)
savedStatusesFileArgs, tabPosition, fromUser, loadingMore, pinnedIds,
timelineFilter as? UserTimelineFilter)
}
override fun onStatusesLoaded(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?) {
@ -107,7 +124,63 @@ class UserTimelineFragment : ParcelableStatusesFragment() {
super.onStatusesLoaded(loader, data)
}
override fun onFilterClick(holder: TimelineFilterHeaderViewHolder) {
val df = UserTimelineFilterDialogFragment()
df.setTargetFragment(this, REQUEST_SET_TIMELINE_FILTER)
df.show(childFragmentManager, "set_timeline_filter")
}
private fun reloadAllStatuses() {
adapterData = null
triggerRefresh()
showProgress()
}
interface UserTimelineFragmentDelegate {
val pinnedStatusIds: Array<String>?
}
class UserTimelineFilterDialogFragment : BaseDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(context)
val values = resources.getStringArray(R.array.values_user_timeline_filter)
val checkedItems = BooleanArray(values.size) {
val filter = preferences[userTimelineFilterKey]
when (values[it]) {
"replies" -> filter.isIncludeReplies
"retweets" -> filter.isIncludeRetweets
else -> false
}
}
builder.setTitle(R.string.title_user_timeline_filter)
builder.setMultiChoiceItems(R.array.entries_user_timeline_filter, checkedItems, null)
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog as AlertDialog
val listView = dialog.listView
val filter = UserTimelineFilter().apply {
isIncludeRetweets = listView.isItemChecked(values.indexOf("retweets"))
isIncludeReplies = listView.isItemChecked(values.indexOf("replies"))
}
preferences.edit().apply {
this[userTimelineFilterKey] = filter
}.apply()
(targetFragment as UserTimelineFragment).reloadAllStatuses()
}
val dialog = builder.create()
dialog.setOnShowListener {
it as AlertDialog
it.applyTheme()
}
return dialog
}
}
companion object {
const val EXTRA_ENABLE_TIMELINE_FILTER = "enable_timeline_filter"
const val REQUEST_SET_TIMELINE_FILTER = 101
}
}

View File

@ -33,6 +33,7 @@ import org.mariotaku.twidere.R
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.timeline.UserTimelineFilter
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import java.util.concurrent.atomic.AtomicReference
@ -50,7 +51,7 @@ class UserTimelineLoader(
fromUser: Boolean,
loadingMore: Boolean,
val pinnedStatusIds: Array<String>?,
val excludeReplies: Boolean = false
val timelineFilter: UserTimelineFilter? = null
) : MicroBlogAPIStatusesLoader(context, accountId, sinceId, maxId, -1, data, savedStatusesArgs,
tabPosition, fromUser, loadingMore) {
@ -80,7 +81,10 @@ class UserTimelineLoader(
}
}
val option = TimelineOption()
option.setExcludeReplies(excludeReplies)
if (timelineFilter != null) {
option.setExcludeReplies(!timelineFilter.isIncludeReplies)
option.setIncludeRetweets(timelineFilter.isIncludeRetweets)
}
if (userId != null) {
return microBlog.getUserTimeline(userId.id, paging, option)
} else if (screenName != null) {

View File

@ -4,6 +4,7 @@ import android.content.Context
import com.squareup.otto.Bus
import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.twidere.model.DefaultFeatures
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import org.mariotaku.twidere.util.media.MediaPreloader
@ -38,6 +39,8 @@ abstract class BaseAbstractTask<Params, Result, Callback>(val context: Context)
@Inject
lateinit var extraFeaturesService: ExtraFeaturesService
@Inject
lateinit var defaultFeatures: DefaultFeatures
@Inject
lateinit var scheduleProviderFactory: StatusScheduleProvider.Factory
val scheduleProvider: StatusScheduleProvider?

View File

@ -14,6 +14,7 @@ import android.support.annotation.WorkerThread
import android.text.TextUtils
import android.webkit.MimeTypeMap
import com.bumptech.glide.Glide
import com.twitter.Validator
import edu.tsinghua.hotmobi.HotMobiLogger
import edu.tsinghua.hotmobi.model.MediaUploadEvent
import net.ypresto.androidtranscoder.MediaTranscoder
@ -36,6 +37,7 @@ import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.extension.getTweetLength
import org.mariotaku.twidere.extension.model.mediaSizeLimit
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.model.textLimit
@ -217,13 +219,16 @@ class UpdateStatusTask(
update: ParcelableStatusUpdate,
pending: PendingStatusUpdate) {
if (shortener == null) return
val validator = Validator()
stateCallback.onShorteningStatus()
val sharedShortened = HashMap<UserKey, StatusShortenResult>()
for (i in 0 until pending.length) {
val account = update.accounts[i]
val text = pending.overrideTexts[i]
val textLimit = account.textLimit
if (textLimit >= 0 && text.length <= textLimit) {
val ignoreMentions = update.in_reply_to_status != null && account.type ==
AccountType.TWITTER && defaultFeatures.isMentionsCountsInStatus
if (textLimit >= 0 && validator.getTweetLength(text, ignoreMentions) <= textLimit) {
continue
}
shortener.waitForService()
@ -403,6 +408,9 @@ class UpdateStatusTask(
status.location(ParcelableLocationUtils.toGeoLocation(statusUpdate.location))
status.displayCoordinates(statusUpdate.display_coordinates)
}
if (statusUpdate.accounts[index].type == AccountType.TWITTER) {
status.autoPopulateReplyMetadata(true)
}
val mediaIds = pendingUpdate.mediaIds[index]
if (mediaIds != null) {
status.mediaIds(*mediaIds)

View File

@ -0,0 +1,51 @@
/*
* 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.view.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.synthetic.main.header_user_timeline_filter.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.model.timeline.TimelineFilter
/**
* Created by mariotaku on 2017/3/31.
*/
class TimelineFilterHeaderViewHolder(val adapter: IStatusesAdapter<*>, itemView: View) : RecyclerView.ViewHolder(itemView) {
private val filterLabel = itemView.filterLabel
private val filterButton = itemView.filterButton
init {
filterButton.setOnClickListener {
adapter.statusClickListener?.onFilterClick(this)
}
}
companion object {
const val layoutResource = R.layout.header_user_timeline_filter
}
fun display(filter: TimelineFilter) {
filterLabel.text = filter.getSummary(itemView.context)
}
}

View File

@ -28,6 +28,7 @@ import org.mariotaku.twidere.model.ParcelableMedia
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.view.CardMediaContainer
import org.mariotaku.twidere.view.holder.TimelineFilterHeaderViewHolder
/**
* Created by mariotaku on 15/10/26.
@ -62,6 +63,8 @@ interface IStatusViewHolder : CardMediaContainer.OnMediaClickListener {
fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean = false
fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {}
fun onFilterClick(holder: TimelineFilterHeaderViewHolder) {}
}
}

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="@dimen/element_size_msmall"
android:orientation="horizontal"
android:padding="@dimen/element_spacing_small">
<TextView
android:id="@+id/filterLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:padding="@dimen/element_spacing_normal"
tools:text="Tweets, retweets, replies"/>
<org.mariotaku.twidere.view.IconActionButton
android:id="@+id/filterButton"
android:layout_width="@dimen/element_size_msmall"
android:layout_height="@dimen/element_size_msmall"
android:layout_weight="0"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_action_filter"
app:iabColor="?android:textColorSecondary"/>
</LinearLayout>

View File

@ -80,4 +80,8 @@
<item>List</item>
<item>List timeline</item>
</string-array>
<string-array name="entries_user_timeline_filter">
<item>Replies</item>
<item>Retweets</item>
</string-array>
</resources>

View File

@ -103,4 +103,8 @@
<item>list</item>
<item>list_timeline</item>
</string-array>
<string-array name="values_user_timeline_filter">
<item>replies</item>
<item>retweets</item>
</string-array>
</resources>

View File

@ -1289,4 +1289,9 @@
<string name="users_lists_with_name"><xliff:g id="name">%s</xliff:g>\'s lists</string>
<string name="users_statuses">User\'s tweets</string>
<string name="hint_search_gif">Search GIF</string>
<string name="title_user_timeline_filter">Timeline filter</string>
<string name="label_statuses">Tweets</string>
<string name="label_statuses_retweets">Tweets and retweets</string>
<string name="label_statuses_replies">Tweets and replies</string>
<string name="label_statuses_retweets_replies">Tweets, retweets and replies</string>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>ic_action_filter-mdpi</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Action-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ic_action_filter-mdpi">
<path d="M14,22 L18,22 L18,20 L14,20 L14,22 Z M7,10 L7,12 L25,12 L25,10 L7,10 Z M10,17 L22,17 L22,15 L10,15 L10,17 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
<polygon id="Shape" points="4 4 28 4 28 28 4 28"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 774 B