diff --git a/build.gradle b/build.gradle index d1d684cce..061c4c8bb 100644 --- a/build.gradle +++ b/build.gradle @@ -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' ] } diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/model/StatusUpdate.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/model/StatusUpdate.java index fdc676914..05cf59bce 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/model/StatusUpdate.java +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/model/StatusUpdate.java @@ -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; } diff --git a/twidere/build.gradle b/twidere/build.gradle index e6f30e85c..626a6ad36 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -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']}" diff --git a/twidere/src/main/java/org/mariotaku/twidere/model/DefaultFeatures.java b/twidere/src/main/java/org/mariotaku/twidere/model/DefaultFeatures.java index d4491fb3f..0659eb3cf 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/model/DefaultFeatures.java +++ b/twidere/src/main/java/org/mariotaku/twidere/model/DefaultFeatures.java @@ -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; } diff --git a/twidere/src/main/java/org/mariotaku/twidere/model/timeline/TimelineFilter.java b/twidere/src/main/java/org/mariotaku/twidere/model/timeline/TimelineFilter.java new file mode 100644 index 000000000..1d8965718 --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/model/timeline/TimelineFilter.java @@ -0,0 +1,31 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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); +} diff --git a/twidere/src/main/java/org/mariotaku/twidere/model/timeline/UserTimelineFilter.java b/twidere/src/main/java/org/mariotaku/twidere/model/timeline/UserTimelineFilter.java new file mode 100644 index 000000000..98f9eaa8b --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/model/timeline/UserTimelineFilter.java @@ -0,0 +1,88 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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 CREATOR = new Creator() { + public UserTimelineFilter createFromParcel(Parcel source) { + UserTimelineFilter target = new UserTimelineFilter(); + UserTimelineFilterParcelablePlease.readFromParcel(target, source); + return target; + } + + public UserTimelineFilter[] newArray(int size) { + return new UserTimelineFilter[size]; + } + }; +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt index 0fc11d18f..dba38f379 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt @@ -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) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt index 46aea2de8..115635a39 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt @@ -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(context, requestManager), IStatusesAdapter>, - IItemCountsAdapter { +) : LoadMoreSupportAdapter(context, requestManager), + IStatusesAdapter>, 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? = 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 getFieldValue(position: Int, - readCursorValueAction: (cursor: Cursor, indices: ParcelableStatusCursorIndices) -> T, + readCursorValueAction: (cursor: Cursor, indices: ObjectCursor.CursorIndices) -> 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 } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/constant/PreferenceKeys.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/constant/PreferenceKeys.kt index 3bb0088bd..028cc3d0a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/constant/PreferenceKeys.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/constant/PreferenceKeys.kt @@ -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.* @@ -241,4 +243,29 @@ object composeAccountsKey : KSimpleKey?>(KEY_COMPOSE_ACCOUNTS, nu return true } +} + +object userTimelineFilterKey : KSimpleKey("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().apply { + if (value.isIncludeReplies) { + add("replies") + } + if (value.isIncludeRetweets) { + add("retweets") + } + }.joinToString(",") + editor.putString(key, options) + return true + } + } \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt index 0ded8e189..996fde720 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt @@ -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(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) - }, 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 = UserTimelineFragment::class.java, args = Bundle(tabArgs).apply { + this[UserTimelineFragment.EXTRA_ENABLE_TIMELINE_FILTER] = true + }, 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, diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt index 5f30879d6..2319b50ae 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt @@ -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 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserTimelineFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserTimelineFragment.kt index 59574775d..dd5946a02 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserTimelineFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserTimelineFragment.kt @@ -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? get() { - val args = arguments!! - val accountKey = Utils.getAccountKey(context, args)!! - val userKey = args.getParcelable(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(EXTRA_USER_KEY) + val screenName = arguments.getString(EXTRA_SCREEN_NAME) val result = ArrayList() 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(EXTRA_USER_KEY) - val screenName = args.getString(EXTRA_SCREEN_NAME) + val userKey = arguments.getParcelable(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?> { 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?>, data: List?) { @@ -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? + + } + + 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 } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/UserTimelineLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/UserTimelineLoader.kt index 22b135668..0d4cbd55b 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/UserTimelineLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/UserTimelineLoader.kt @@ -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?, - 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) { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/BaseAbstractTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/BaseAbstractTask.kt index bd74923d5..f985fdd21 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/BaseAbstractTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/BaseAbstractTask.kt @@ -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(val context: Context) @Inject lateinit var extraFeaturesService: ExtraFeaturesService @Inject + lateinit var defaultFeatures: DefaultFeatures + @Inject lateinit var scheduleProviderFactory: StatusScheduleProvider.Factory val scheduleProvider: StatusScheduleProvider? diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt index 4f0617f7c..f6fcf45b0 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt @@ -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() 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) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/TimelineFilterHeaderViewHolder.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/TimelineFilterHeaderViewHolder.kt new file mode 100644 index 000000000..5146da0cb --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/TimelineFilterHeaderViewHolder.kt @@ -0,0 +1,51 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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) + } +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.kt index 4ab364047..983ae4b5c 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.kt @@ -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) {} } } diff --git a/twidere/src/main/res/layout/header_user_timeline_filter.xml b/twidere/src/main/res/layout/header_user_timeline_filter.xml new file mode 100644 index 000000000..2837ba884 --- /dev/null +++ b/twidere/src/main/res/layout/header_user_timeline_filter.xml @@ -0,0 +1,50 @@ + + + + + + + + + + \ No newline at end of file diff --git a/twidere/src/main/res/values/arrays.xml b/twidere/src/main/res/values/arrays.xml index e48039de2..f76548788 100644 --- a/twidere/src/main/res/values/arrays.xml +++ b/twidere/src/main/res/values/arrays.xml @@ -80,4 +80,8 @@ List List timeline + + Replies + Retweets + diff --git a/twidere/src/main/res/values/arrays_no_translate.xml b/twidere/src/main/res/values/arrays_no_translate.xml index d86856742..fcb60de8f 100644 --- a/twidere/src/main/res/values/arrays_no_translate.xml +++ b/twidere/src/main/res/values/arrays_no_translate.xml @@ -103,4 +103,8 @@ list list_timeline + + replies + retweets + \ No newline at end of file diff --git a/twidere/src/main/res/values/strings.xml b/twidere/src/main/res/values/strings.xml index 95c5a4955..cb829dcf4 100644 --- a/twidere/src/main/res/values/strings.xml +++ b/twidere/src/main/res/values/strings.xml @@ -1289,4 +1289,9 @@ %s\'s lists User\'s tweets Search GIF + Timeline filter + Tweets + Tweets and retweets + Tweets and replies + Tweets, retweets and replies diff --git a/twidere/src/main/svg/drawable/ic_action_filter-mdpi.svg b/twidere/src/main/svg/drawable/ic_action_filter-mdpi.svg new file mode 100644 index 000000000..7fe3d281a --- /dev/null +++ b/twidere/src/main/svg/drawable/ic_action_filter-mdpi.svg @@ -0,0 +1,13 @@ + + + + ic_action_filter-mdpi + Created with Sketch. + + + + + + + + \ No newline at end of file