From 45a65d099f07647832d6977b946ea0d8b88db0ef Mon Sep 17 00:00:00 2001 From: Mariotaku Lee Date: Mon, 28 Aug 2017 17:56:09 +0800 Subject: [PATCH] moved translation settings --- .../TranslationDestinationPreference.java | 223 -- .../twidere/activity/LinkHandlerActivity.kt | 7 +- .../twidere/adapter/StatusDetailsAdapter.kt | 548 +++++ .../twidere/constant/PreferenceKeys.kt | 1 + .../twidere/fragment/StatusFragment.kt | 2139 ----------------- .../twidere/fragment/status/StatusFragment.kt | 960 ++++++++ .../TranslationDestinationDialogFragment.kt | 114 + .../holder/status/DetailStatusViewHolder.kt | 760 ++++++ twidere/src/main/res/layout/header_status.xml | 33 +- twidere/src/main/res/values/strings.xml | 5 +- .../src/main/res/xml/preferences_content.xml | 3 - 11 files changed, 2420 insertions(+), 2373 deletions(-) delete mode 100644 twidere/src/main/java/org/mariotaku/twidere/preference/TranslationDestinationPreference.java create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/adapter/StatusDetailsAdapter.kt delete mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/TranslationDestinationDialogFragment.kt create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt diff --git a/twidere/src/main/java/org/mariotaku/twidere/preference/TranslationDestinationPreference.java b/twidere/src/main/java/org/mariotaku/twidere/preference/TranslationDestinationPreference.java deleted file mode 100644 index a68b10a0b..000000000 --- a/twidere/src/main/java/org/mariotaku/twidere/preference/TranslationDestinationPreference.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2014 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.preference; - -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.os.AsyncTask; -import android.support.annotation.NonNull; -import android.support.v7.app.AlertDialog; -import android.support.v7.preference.Preference; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import org.mariotaku.microblog.library.MicroBlog; -import org.mariotaku.microblog.library.MicroBlogException; -import org.mariotaku.microblog.library.twitter.model.Language; -import org.mariotaku.microblog.library.twitter.model.ResponseList; -import org.mariotaku.twidere.R; -import org.mariotaku.twidere.extension.DialogExtensionsKt; -import org.mariotaku.twidere.util.MicroBlogAPIFactory; - -import java.text.Collator; -import java.util.Comparator; -import java.util.List; - -import static org.mariotaku.twidere.TwidereConstants.LOGTAG; - -/** - * @deprecated This will be removed soon, target language selection will be changed inside - * translation UI. - */ -@Deprecated -public class TranslationDestinationPreference extends Preference implements OnClickListener { - - private String mSelectedLanguageCode = "en"; - - private GetLanguagesTask mGetAvailableTrendsTask; - - private final LanguagesAdapter mAdapter; - - private AlertDialog mDialog; - - public TranslationDestinationPreference(final Context context) { - this(context, null); - } - - public TranslationDestinationPreference(final Context context, final AttributeSet attrs) { - this(context, attrs, R.attr.preferenceStyle); - } - - public TranslationDestinationPreference(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - mAdapter = new LanguagesAdapter(context); - } - - @Override - public void onClick(final DialogInterface dialog, final int which) { - final Language item = mAdapter.getItem(which); - if (item != null) { - persistString(item.getCode()); - } - if (mDialog != null && mDialog.isShowing()) { - mDialog.dismiss(); - } - } - - @Override - protected void onClick() { - if (mGetAvailableTrendsTask != null) { - mGetAvailableTrendsTask.cancel(false); - } - mGetAvailableTrendsTask = new GetLanguagesTask(getContext()); - mGetAvailableTrendsTask.execute(); - } - - private static class LanguageComparator implements Comparator { - private final Collator mCollator; - - LanguageComparator(final Context context) { - mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); - } - - @Override - public int compare(final Language object1, final Language object2) { - return mCollator.compare(object1.getName(), object2.getName()); - } - - } - - private static class LanguagesAdapter extends ArrayAdapter { - - private final Context mContext; - - public LanguagesAdapter(final Context context) { - super(context, android.R.layout.simple_list_item_single_choice); - mContext = context; - } - - public int findItemPosition(final String code) { - if (TextUtils.isEmpty(code)) return -1; - final int count = getCount(); - for (int i = 0; i < count; i++) { - final Language item = getItem(i); - if (code.equalsIgnoreCase(item.getCode())) return i; - } - return -1; - } - - @NonNull - @Override - public View getView(final int position, final View convertView, @NonNull final ViewGroup parent) { - final View view = super.getView(position, convertView, parent); - final TextView text = (TextView) (view instanceof TextView ? view : view.findViewById(android.R.id.text1)); - final Language item = getItem(position); - if (item != null && text != null) { - text.setSingleLine(); - text.setText(item.getName()); - } - return view; - } - - public void setData(final List data) { - clear(); - if (data != null) { - addAll(data); - } - sort(new LanguageComparator(mContext)); - } - - } - - class GetLanguagesTask extends AsyncTask> implements OnCancelListener { - - private final ProgressDialog mProgress; - - public GetLanguagesTask(final Context context) { - mProgress = new ProgressDialog(context); - } - - @Override - public void onCancel(final DialogInterface dialog) { - cancel(true); - } - - @Override - protected ResponseList doInBackground(final Object... args) { - final MicroBlog twitter = MicroBlogAPIFactory.getDefaultTwitterInstance(getContext()); - if (twitter == null) return null; - try { - mSelectedLanguageCode = twitter.getAccountSettings().getLanguage(); - return twitter.getLanguages(); - } catch (final MicroBlogException e) { - Log.w(LOGTAG, e); - } - return null; - } - - @Override - protected void onPostExecute(final ResponseList result) { - if (mProgress.isShowing()) { - mProgress.dismiss(); - } - mAdapter.setData(result); - if (result == null) return; - final AlertDialog.Builder selectorBuilder = new AlertDialog.Builder(getContext()); - selectorBuilder.setTitle(getTitle()); - final String value = getPersistedString(mSelectedLanguageCode); - selectorBuilder.setSingleChoiceItems(mAdapter, mAdapter.findItemPosition(value), - TranslationDestinationPreference.this); - selectorBuilder.setNegativeButton(android.R.string.cancel, null); - mDialog = selectorBuilder.create(); - mDialog.setOnShowListener(new DialogInterface.OnShowListener() { - @Override - public void onShow(final DialogInterface dialog) { - final AlertDialog alertDialog = (AlertDialog) dialog; - DialogExtensionsKt.applyTheme(alertDialog); - final ListView lv = alertDialog.getListView(); - if (lv != null) { - lv.setFastScrollEnabled(true); - } - } - }); - mDialog.show(); - } - - @Override - protected void onPreExecute() { - if (mProgress.isShowing()) { - mProgress.dismiss(); - } - mProgress.setMessage(getContext().getString(R.string.message_please_wait)); - mProgress.setOnCancelListener(this); - mProgress.show(); - } - - } -} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt index 334188bc9..3bf06409a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/LinkHandlerActivity.kt @@ -65,6 +65,7 @@ import org.mariotaku.twidere.fragment.message.MessagesConversationFragment import org.mariotaku.twidere.fragment.message.MessagesEntriesFragment import org.mariotaku.twidere.fragment.search.MastodonSearchFragment import org.mariotaku.twidere.fragment.search.SearchFragment +import org.mariotaku.twidere.fragment.status.StatusFragment import org.mariotaku.twidere.fragment.statuses.* import org.mariotaku.twidere.fragment.users.* import org.mariotaku.twidere.graphic.ActionBarColorDrawable @@ -167,7 +168,7 @@ class LinkHandlerActivity : BaseActivity(), SystemWindowInsetsCallback, IControl ft.commit() } setTitle(linkId, uri) - finishOnly = uri.getQueryParameter(QUERY_PARAM_FINISH_ONLY)?.toBoolean() ?: false + finishOnly = uri.getQueryParameter(QUERY_PARAM_FINISH_ONLY)?.toBoolean() == true supportActionBar?.setBackgroundDrawable(ActionBarColorDrawable.create(overrideTheme.colorToolbar, true)) @@ -913,8 +914,8 @@ class LinkHandlerActivity : BaseActivity(), SystemWindowInsetsCallback, IControl } interface HideUiOnScroll - - private fun Uri.getUserKeyQueryParameter() : UserKey? { + + private fun Uri.getUserKeyQueryParameter(): UserKey? { val value = getQueryParameter(QUERY_PARAM_USER_KEY) ?: getQueryParameter(QUERY_PARAM_USER_ID) return value?.let(UserKey::valueOf) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/StatusDetailsAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/StatusDetailsAdapter.kt new file mode 100644 index 000000000..923ab8972 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/StatusDetailsAdapter.kt @@ -0,0 +1,548 @@ +/* + * 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.adapter + +import android.support.v7.widget.RecyclerView +import android.text.TextUtils +import android.text.method.LinkMovementMethod +import android.util.SparseBooleanArray +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Space +import android.widget.TextView +import com.bumptech.glide.Glide +import org.mariotaku.kpreferences.get +import org.mariotaku.ktextension.contains +import org.mariotaku.microblog.library.twitter.model.TranslationResult +import org.mariotaku.twidere.R +import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter +import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter +import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter +import org.mariotaku.twidere.adapter.iface.IStatusesAdapter +import org.mariotaku.twidere.constant.* +import org.mariotaku.twidere.extension.model.originalId +import org.mariotaku.twidere.fragment.status.StatusFragment +import org.mariotaku.twidere.model.* +import org.mariotaku.twidere.util.StatusAdapterLinkClickHandler +import org.mariotaku.twidere.util.ThemeUtils +import org.mariotaku.twidere.util.TwidereLinkify +import org.mariotaku.twidere.view.holder.EmptyViewHolder +import org.mariotaku.twidere.view.holder.LoadIndicatorViewHolder +import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder +import org.mariotaku.twidere.view.holder.status.DetailStatusViewHolder + +class StatusDetailsAdapter( + val fragment: StatusFragment +) : LoadMoreSupportAdapter(fragment.context, Glide.with(fragment)), + IStatusesAdapter>, IItemCountsAdapter { + + override val twidereLinkify: TwidereLinkify + + override var statusClickListener: IStatusViewHolder.StatusClickListener? = null + + override val itemCounts = ItemCounts(ITEM_TYPES_SUM) + + override val nameFirst = preferences[nameFirstKey] + override val mediaPreviewStyle = preferences[mediaPreviewStyleKey] + override val linkHighlightingStyle = preferences[linkHighlightOptionKey] + override val lightFont = preferences[lightFontKey] + override val mediaPreviewEnabled = preferences[mediaPreviewKey] + override val sensitiveContentEnabled = preferences[displaySensitiveContentsKey] + override val useStarsForLikes = preferences[iWantMyStarsBackKey] + + private val inflater: LayoutInflater + private val cardBackgroundColor: Int + private val showCardActions = !preferences[hideCardActionsKey] + private var recyclerView: RecyclerView? = null + private var detailMediaExpanded: Boolean = false + + var status: ParcelableStatus? = null + internal set + var translationResult: TranslationResult? = null + internal set(translation) { + if (translation == null || status?.originalId != translation.id) { + field = null + } else { + field = translation + } + notifyDataSetChanged() + } + var statusActivity: StatusFragment.StatusActivity? = null + internal set(value) { + val status = status ?: return + if (value != null && !value.isStatus(status)) { + return + } + field = value + val statusIndex = getIndexStart(ITEM_IDX_STATUS) + notifyItemChanged(statusIndex, value) + } + var statusAccount: AccountDetails? = null + internal set + + private var data: List? = null + private var replyError: CharSequence? = null + private var conversationError: CharSequence? = null + private var replyStart: Int = 0 + private var showingActionCardPosition = RecyclerView.NO_POSITION + private val showingFullTextStates = SparseBooleanArray() + + init { + setHasStableIds(true) + val context = fragment.activity + // There's always a space at the end of the list + itemCounts[ITEM_IDX_SPACE] = 1 + itemCounts[ITEM_IDX_STATUS] = 1 + itemCounts[ITEM_IDX_CONVERSATION_LOAD_MORE] = 1 + itemCounts[ITEM_IDX_REPLY_LOAD_MORE] = 1 + inflater = LayoutInflater.from(context) + cardBackgroundColor = ThemeUtils.getCardBackgroundColor(context, + preferences[themeBackgroundOptionKey], preferences[themeBackgroundAlphaKey]) + val listener = StatusAdapterLinkClickHandler>(context, preferences) + listener.setAdapter(this) + twidereLinkify = TwidereLinkify(listener) + } + + override fun getStatus(position: Int, raw: Boolean): ParcelableStatus { + when (getItemCountIndex(position, raw)) { + ITEM_IDX_CONVERSATION -> { + var idx = position - getIndexStart(ITEM_IDX_CONVERSATION) + if (data!![idx].is_filtered) idx++ + return data!![idx] + } + ITEM_IDX_REPLY -> { + var idx = position - getIndexStart(ITEM_IDX_CONVERSATION) - + getTypeCount(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_STATUS) + + replyStart + if (data!![idx].is_filtered) idx++ + return data!![idx] + } + ITEM_IDX_STATUS -> { + return status!! + } + } + throw IndexOutOfBoundsException("index: $position") + } + + fun getIndexStart(index: Int): Int { + if (index == 0) return 0 + return itemCounts.getItemStartPosition(index) + } + + override fun getStatusId(position: Int, raw: Boolean): String { + return getStatus(position, raw).id + } + + override fun getStatusTimestamp(position: Int, raw: Boolean): Long { + return getStatus(position, raw).timestamp + } + + override fun getStatusPositionKey(position: Int, raw: Boolean): Long { + val status = getStatus(position, raw) + return if (status.position_key > 0) status.timestamp else getStatusTimestamp(position, raw) + } + + override fun getAccountKey(position: Int, raw: Boolean) = getStatus(position, raw).account_key + + override fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? { + if (status != null && accountKey == status!!.account_key && TextUtils.equals(statusId, status!!.id)) { + return status + } + return data?.firstOrNull { accountKey == it.account_key && TextUtils.equals(it.id, statusId) } + } + + override fun getStatusCount(raw: Boolean): Int { + return getTypeCount(ITEM_IDX_CONVERSATION) + getTypeCount(ITEM_IDX_STATUS) + getTypeCount(ITEM_IDX_REPLY) + } + + override fun isCardActionsShown(position: Int): Boolean { + if (position == RecyclerView.NO_POSITION) return showCardActions + return showCardActions || showingActionCardPosition == position + } + + override fun showCardActions(position: Int) { + if (showingActionCardPosition != RecyclerView.NO_POSITION) { + notifyItemChanged(showingActionCardPosition) + } + showingActionCardPosition = position + if (position != RecyclerView.NO_POSITION) { + notifyItemChanged(position) + } + } + + override fun isFullTextVisible(position: Int): Boolean { + return showingFullTextStates.get(position) + } + + override fun setFullTextVisible(position: Int, visible: Boolean) { + showingFullTextStates.put(position, visible) + if (position != RecyclerView.NO_POSITION) { + notifyItemChanged(position) + } + } + + override fun setData(data: List?): Boolean { + val status = this.status ?: return false + val changed = this.data != data + this.data = data + if (data == null || data.isEmpty()) { + setTypeCount(ITEM_IDX_CONVERSATION, 0) + setTypeCount(ITEM_IDX_REPLY, 0) + replyStart = -1 + } else { + var sortId = status.sort_id + + if (status.is_retweet) { + sortId = data.find { + it.id == status.retweet_id + }?.sort_id ?: status.retweet_timestamp + } + var conversationCount = 0 + var replyCount = 0 + var replyStart = -1 + data.forEachIndexed { i, item -> + if (item.sort_id < sortId) { + if (!item.is_filtered) { + conversationCount++ + } + } else if (status.id == item.id) { + this.status = item + } else if (item.sort_id > sortId) { + if (replyStart < 0) { + replyStart = i + } + if (!item.is_filtered) { + replyCount++ + } + } + } + setTypeCount(ITEM_IDX_CONVERSATION, conversationCount) + setTypeCount(ITEM_IDX_REPLY, replyCount) + this.replyStart = replyStart + } + notifyDataSetChanged() + updateItemDecoration() + return changed + } + + override val showAccountsColor: Boolean + get() = false + + var isDetailMediaExpanded: Boolean + get() { + if (detailMediaExpanded) return true + if (mediaPreviewEnabled) { + val status = this.status + return status != null && (sensitiveContentEnabled || !status.is_possibly_sensitive) + } + return false + } + set(expanded) { + detailMediaExpanded = expanded + notifyDataSetChanged() + updateItemDecoration() + } + + override fun isGapItem(position: Int): Boolean { + return false + } + + override val gapClickListener: IGapSupportedAdapter.GapClickListener? + get() = statusClickListener + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? { + when (viewType) { + VIEW_TYPE_DETAIL_STATUS -> { + val view = inflater.inflate(R.layout.header_status, parent, false) + view.setBackgroundColor(cardBackgroundColor) + return DetailStatusViewHolder(this, view) + } + VIEW_TYPE_LIST_STATUS -> { + return ListParcelableStatusesAdapter.createStatusViewHolder(this, inflater, parent) + } + VIEW_TYPE_CONVERSATION_LOAD_INDICATOR, VIEW_TYPE_REPLIES_LOAD_INDICATOR -> { + val view = inflater.inflate(R.layout.list_item_load_indicator, parent, + false) + return LoadIndicatorViewHolder(view) + } + VIEW_TYPE_SPACE -> { + return EmptyViewHolder(Space(context)) + } + VIEW_TYPE_REPLY_ERROR -> { + val view = inflater.inflate(R.layout.adapter_item_status_error, parent, + false) + return StatusErrorItemViewHolder(view) + } + } + return null + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { + var handled = false + when (holder.itemViewType) { + VIEW_TYPE_DETAIL_STATUS -> { + holder as DetailStatusViewHolder + payloads.forEach { it -> + when (it) { + is StatusFragment.StatusActivity -> { + holder.updateStatusActivity(it) + } + is ParcelableStatus -> { + holder.displayStatus(statusAccount, status, statusActivity, + translationResult) + } + } + handled = true + } + } + } + if (handled) return + super.onBindViewHolder(holder, position, payloads) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder.itemViewType) { + VIEW_TYPE_DETAIL_STATUS -> { + val status = getStatus(position) + val detailHolder = holder as DetailStatusViewHolder + detailHolder.displayStatus(statusAccount, status, statusActivity, translationResult) + } + VIEW_TYPE_LIST_STATUS -> { + val status = getStatus(position) + val statusHolder = holder as IStatusViewHolder + // Display 'in reply to' for first item + // useful to indicate whether first tweet has reply or not + // We only display that indicator for first conversation item + val itemType = getItemType(position) + val displayInReplyTo = itemType == ITEM_IDX_CONVERSATION && position - getItemTypeStart(position) == 0 + statusHolder.display(status = status, displayInReplyTo = displayInReplyTo) + } + VIEW_TYPE_REPLY_ERROR -> { + val errorHolder = holder as StatusErrorItemViewHolder + errorHolder.showError(replyError!!) + } + VIEW_TYPE_CONVERSATION_ERROR -> { + val errorHolder = holder as StatusErrorItemViewHolder + errorHolder.showError(conversationError!!) + } + VIEW_TYPE_CONVERSATION_LOAD_INDICATOR -> { + val indicatorHolder = holder as LoadIndicatorViewHolder + indicatorHolder.setLoadProgressVisible(isConversationsLoading) + } + VIEW_TYPE_REPLIES_LOAD_INDICATOR -> { + val indicatorHolder = holder as LoadIndicatorViewHolder + indicatorHolder.setLoadProgressVisible(isRepliesLoading) + } + } + } + + override fun getItemViewType(position: Int): Int { + return getItemViewTypeByItemType(getItemType(position)) + } + + override fun addGapLoadingId(id: ObjectId) { + + } + + override fun removeGapLoadingId(id: ObjectId) { + + } + + private fun getItemViewTypeByItemType(type: Int): Int { + when (type) { + ITEM_IDX_CONVERSATION, ITEM_IDX_REPLY -> return VIEW_TYPE_LIST_STATUS + ITEM_IDX_CONVERSATION_LOAD_MORE -> return VIEW_TYPE_CONVERSATION_LOAD_INDICATOR + ITEM_IDX_REPLY_LOAD_MORE -> return VIEW_TYPE_REPLIES_LOAD_INDICATOR + ITEM_IDX_STATUS -> return VIEW_TYPE_DETAIL_STATUS + ITEM_IDX_SPACE -> return VIEW_TYPE_SPACE + ITEM_IDX_REPLY_ERROR -> return VIEW_TYPE_REPLY_ERROR + ITEM_IDX_CONVERSATION_ERROR -> return VIEW_TYPE_CONVERSATION_ERROR + } + throw IllegalStateException() + } + + private fun getItemCountIndex(position: Int, raw: Boolean): Int { + return itemCounts.getItemCountIndex(position) + } + + fun getItemType(position: Int): Int { + var typeStart = 0 + for (type in 0 until ITEM_TYPES_SUM) { + val typeCount = getTypeCount(type) + val typeEnd = typeStart + typeCount + if (position in typeStart until typeEnd) return type + typeStart = typeEnd + } + throw IllegalStateException("Unknown position " + position) + } + + fun getItemTypeStart(position: Int): Int { + var typeStart = 0 + for (type in 0 until ITEM_TYPES_SUM) { + val typeCount = getTypeCount(type) + val typeEnd = typeStart + typeCount + if (position in typeStart until typeEnd) return typeStart + typeStart = typeEnd + } + throw IllegalStateException() + } + + override fun getItemId(position: Int): Long { + val countIndex = getItemCountIndex(position) + when (countIndex) { + ITEM_IDX_CONVERSATION, ITEM_IDX_STATUS, ITEM_IDX_REPLY -> { + val status = getStatus(position) + val hashCode = ParcelableStatus.calculateHashCode(status.account_key, status.id) + return (countIndex.toLong() shl 32) or hashCode.toLong() + } + } + val countPos = (position - getItemStartPosition(countIndex)).toLong() + return (countIndex.toLong() shl 32) or countPos + } + + override fun getItemCount(): Int { + if (status == null) return 0 + return itemCounts.itemCount + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView?) { + super.onAttachedToRecyclerView(recyclerView) + this.recyclerView = recyclerView + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView?) { + super.onDetachedFromRecyclerView(recyclerView) + this.recyclerView = null + } + + private fun setTypeCount(idx: Int, size: Int) { + itemCounts[idx] = size + notifyDataSetChanged() + } + + fun getTypeCount(idx: Int): Int { + return itemCounts[idx] + } + + fun setReplyError(error: CharSequence?) { + replyError = error + setTypeCount(ITEM_IDX_REPLY_ERROR, if (error != null) 1 else 0) + updateItemDecoration() + } + + fun setConversationError(error: CharSequence?) { + conversationError = error + setTypeCount(ITEM_IDX_CONVERSATION_ERROR, if (error != null) 1 else 0) + updateItemDecoration() + } + + fun setStatus(status: ParcelableStatus, account: AccountDetails?): Boolean { + val oldStatus = this.status + val oldAccount = this.statusAccount + val changed = oldStatus != status && oldAccount != account + this.status = status + this.statusAccount = account + if (changed) { + notifyDataSetChanged() + updateItemDecoration() + } else { + val statusIndex = getIndexStart(ITEM_IDX_STATUS) + notifyItemChanged(statusIndex, status) + } + return changed + } + + fun updateItemDecoration() { + if (recyclerView == null) return + } + + fun getFirstPositionOfItem(itemIdx: Int): Int { + var position = 0 + for (i in 0 until ITEM_TYPES_SUM) { + if (itemIdx == i) return position + position += getTypeCount(i) + } + return RecyclerView.NO_POSITION + } + + + fun getData(): List? { + return data + } + + var isConversationsLoading: Boolean + get() = ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition + set(loading) { + if (loading) { + loadMoreIndicatorPosition = loadMoreIndicatorPosition or ILoadMoreSupportAdapter.START + } else { + loadMoreIndicatorPosition = loadMoreIndicatorPosition and ILoadMoreSupportAdapter.START.inv() + } + updateItemDecoration() + } + + var isRepliesLoading: Boolean + get() = ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition + set(loading) { + if (loading) { + loadMoreIndicatorPosition = loadMoreIndicatorPosition or ILoadMoreSupportAdapter.END + } else { + loadMoreIndicatorPosition = loadMoreIndicatorPosition and ILoadMoreSupportAdapter.END.inv() + } + updateItemDecoration() + } + + class StatusErrorItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val textView = itemView.findViewById(android.R.id.text1) + + init { + textView.movementMethod = LinkMovementMethod.getInstance() + textView.linksClickable = true + } + + fun showError(text: CharSequence) { + textView.text = text + } + } + + companion object { + + const val VIEW_TYPE_LIST_STATUS = 0 + const val VIEW_TYPE_DETAIL_STATUS = 1 + const val VIEW_TYPE_CONVERSATION_LOAD_INDICATOR = 2 + const val VIEW_TYPE_REPLIES_LOAD_INDICATOR = 3 + const val VIEW_TYPE_REPLY_ERROR = 4 + const val VIEW_TYPE_CONVERSATION_ERROR = 5 + const val VIEW_TYPE_SPACE = 6 + const val VIEW_TYPE_EMPTY = 7 + + const val ITEM_IDX_CONVERSATION_LOAD_MORE = 0 + const val ITEM_IDX_CONVERSATION_ERROR = 1 + const val ITEM_IDX_CONVERSATION = 2 + const val ITEM_IDX_STATUS = 3 + const val ITEM_IDX_REPLY = 4 + const val ITEM_IDX_REPLY_ERROR = 5 + const val ITEM_IDX_REPLY_LOAD_MORE = 6 + const val ITEM_IDX_SPACE = 7 + const val ITEM_TYPES_SUM = 8 + } +} \ No newline at end of file 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 3d796eb12..1b8fed5c4 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/constant/PreferenceKeys.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/constant/PreferenceKeys.kt @@ -87,6 +87,7 @@ val composeStatusVisibilityKey = KNullableStringKey("compose_status_visibility", val navbarStyleKey = KStringKey(KEY_NAVBAR_STYLE, NavbarStyle.DEFAULT) val lastLaunchTimeKey = KLongKey("last_launch_time", -1) val promotionsEnabledKey = KBooleanKey("promotions_enabled", false) +val translationDestinationKey = KNullableStringKey(KEY_TRANSLATION_DESTINATION, null) object cacheSizeLimitKey : KSimpleKey(KEY_CACHE_SIZE_LIMIT, 300) { override fun read(preferences: SharedPreferences) = preferences.getInt(key, def).coerceIn(100, diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt deleted file mode 100644 index 351f874be..000000000 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt +++ /dev/null @@ -1,2139 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2014 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.fragment - -import android.accounts.AccountManager -import android.app.Activity -import android.app.Dialog -import android.content.* -import android.graphics.Color -import android.graphics.Rect -import android.nfc.NdefMessage -import android.nfc.NdefRecord -import android.nfc.NfcAdapter.CreateNdefMessageCallback -import android.os.Bundle -import android.support.annotation.UiThread -import android.support.v4.app.LoaderManager.LoaderCallbacks -import android.support.v4.app.hasRunningLoadersSafe -import android.support.v4.content.ContextCompat -import android.support.v4.content.FixedAsyncTaskLoader -import android.support.v4.content.Loader -import android.support.v4.view.MenuItemCompat -import android.support.v4.view.ViewCompat -import android.support.v7.app.AlertDialog -import android.support.v7.widget.ActionMenuView -import android.support.v7.widget.FixedLinearLayoutManager -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.support.v7.widget.RecyclerView.ViewHolder -import android.text.SpannableString -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.TextUtils -import android.text.method.LinkMovementMethod -import android.text.style.ForegroundColorSpan -import android.util.SparseBooleanArray -import android.view.* -import android.view.View.OnClickListener -import android.widget.Space -import android.widget.TextView -import android.widget.Toast -import com.bumptech.glide.Glide -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.adapter_item_status_count_label.view.* -import kotlinx.android.synthetic.main.fragment_status.* -import kotlinx.android.synthetic.main.header_status.view.* -import kotlinx.android.synthetic.main.layout_content_fragment_common.* -import org.mariotaku.abstask.library.TaskStarter -import org.mariotaku.kpreferences.get -import org.mariotaku.ktextension.* -import org.mariotaku.library.objectcursor.ObjectCursor -import org.mariotaku.microblog.library.MicroBlog -import org.mariotaku.microblog.library.MicroBlogException -import org.mariotaku.microblog.library.twitter.model.Paging -import org.mariotaku.microblog.library.twitter.model.TranslationResult -import org.mariotaku.sqliteqb.library.Expression -import org.mariotaku.twidere.Constants.* -import org.mariotaku.twidere.R -import org.mariotaku.twidere.activity.ColorPickerDialogActivity -import org.mariotaku.twidere.adapter.BaseRecyclerViewAdapter -import org.mariotaku.twidere.adapter.ListParcelableStatusesAdapter -import org.mariotaku.twidere.adapter.LoadMoreSupportAdapter -import org.mariotaku.twidere.adapter.decorator.ExtendedDividerItemDecoration -import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter -import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter -import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter -import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition -import org.mariotaku.twidere.adapter.iface.IStatusesAdapter -import org.mariotaku.twidere.annotation.AccountType -import org.mariotaku.twidere.annotation.ProfileImageSize -import org.mariotaku.twidere.constant.* -import org.mariotaku.twidere.constant.KeyboardShortcutConstants.* -import org.mariotaku.twidere.extension.applyTheme -import org.mariotaku.twidere.extension.getErrorMessage -import org.mariotaku.twidere.extension.loadProfileImage -import org.mariotaku.twidere.extension.model.* -import org.mariotaku.twidere.extension.model.api.key -import org.mariotaku.twidere.extension.model.api.toParcelable -import org.mariotaku.twidere.extension.onShow -import org.mariotaku.twidere.extension.view.calculateSpaceItemHeight -import org.mariotaku.twidere.fragment.AbsStatusesFragment.Companion.handleActionClick -import org.mariotaku.twidere.loader.ParcelableStatusLoader -import org.mariotaku.twidere.loader.statuses.ConversationLoader -import org.mariotaku.twidere.menu.FavoriteItemProvider -import org.mariotaku.twidere.model.* -import org.mariotaku.twidere.model.analyzer.Share -import org.mariotaku.twidere.model.analyzer.StatusView -import org.mariotaku.twidere.model.event.FavoriteTaskEvent -import org.mariotaku.twidere.model.event.StatusListChangedEvent -import org.mariotaku.twidere.model.pagination.Pagination -import org.mariotaku.twidere.model.pagination.SinceMaxPagination -import org.mariotaku.twidere.model.util.AccountUtils -import org.mariotaku.twidere.model.util.ParcelableLocationUtils -import org.mariotaku.twidere.model.util.ParcelableMediaUtils -import org.mariotaku.twidere.provider.TwidereDataStore.CachedStatuses -import org.mariotaku.twidere.provider.TwidereDataStore.Statuses -import org.mariotaku.twidere.task.AbsAccountRequestTask -import org.mariotaku.twidere.util.* -import org.mariotaku.twidere.util.ContentScrollHandler.ContentListSupport -import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback -import org.mariotaku.twidere.util.RecyclerViewScrollHandler.RecyclerViewCallback -import org.mariotaku.twidere.util.twitter.card.TwitterCardViewFactory -import org.mariotaku.twidere.view.CardMediaContainer.OnMediaClickListener -import org.mariotaku.twidere.view.ExtendedRecyclerView -import org.mariotaku.twidere.view.ProfileImageView -import org.mariotaku.twidere.view.holder.GapViewHolder -import org.mariotaku.twidere.view.holder.LoadIndicatorViewHolder -import org.mariotaku.twidere.view.holder.StatusViewHolder -import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder -import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder.StatusClickListener -import java.lang.ref.WeakReference -import java.util.* - -/** - * Displays status details - * Created by mariotaku on 14/12/5. - */ -class StatusFragment : BaseFragment(), LoaderCallbacks>, - OnMediaClickListener, StatusClickListener, KeyboardShortcutCallback, - ContentListSupport { - private var mItemDecoration: ExtendedDividerItemDecoration? = null - - override lateinit var adapter: StatusAdapter - - private lateinit var layoutManager: LinearLayoutManager - private lateinit var navigationHelper: RecyclerViewNavigationHelper - private lateinit var scrollListener: RecyclerViewScrollHandler - - private var loadTranslationTask: LoadTranslationTask? = null - // Data fields - private var conversationLoaderInitialized: Boolean = false - - private var activityLoaderInitialized: Boolean = false - private var hasMoreConversation = true - - // Listeners - private val conversationsLoaderCallback = object : LoaderCallbacks> { - override fun onCreateLoader(id: Int, args: Bundle): Loader> { - val adapter = this@StatusFragment.adapter - adapter.isRepliesLoading = true - adapter.isConversationsLoading = true - adapter.updateItemDecoration() - val status: ParcelableStatus = args.getParcelable(EXTRA_STATUS) - val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false) - return ConversationLoader(activity, status, adapter.getData(), true, loadingMore).apply { - pagination = args.toPagination() - // Setting comparator to null lets statuses sort ascending - comparator = null - } - } - - override fun onLoadFinished(loader: Loader>, data: List?) { - val adapter = this@StatusFragment.adapter - adapter.updateItemDecoration() - val conversationLoader = loader as ConversationLoader - var supportedPositions: Long = 0 - if (data != null && !data.isEmpty()) { - val sinceSortId = (conversationLoader.pagination as? SinceMaxPagination)?.sinceSortId ?: -1 - if (sinceSortId < data[data.size - 1].sort_id) { - supportedPositions = supportedPositions or ILoadMoreSupportAdapter.END - } - if (data[0].in_reply_to_status_id != null) { - supportedPositions = supportedPositions or ILoadMoreSupportAdapter.START - } - } else { - supportedPositions = supportedPositions or ILoadMoreSupportAdapter.END - val status = status - if (status != null && status.in_reply_to_status_id != null) { - supportedPositions = supportedPositions or ILoadMoreSupportAdapter.START - } - } - adapter.loadMoreSupportedPosition = supportedPositions - setConversation(data) - adapter.isConversationsLoading = false - adapter.isRepliesLoading = false - } - - override fun onLoaderReset(loader: Loader>) { - - } - } - - private val statusActivityLoaderCallback = object : LoaderCallbacks { - override fun onCreateLoader(id: Int, args: Bundle): Loader { - val accountKey = args.getParcelable(EXTRA_ACCOUNT_KEY) - val statusId = args.getString(EXTRA_STATUS_ID) - return StatusActivitySummaryLoader(activity, accountKey, statusId) - } - - override fun onLoadFinished(loader: Loader, data: StatusActivity?) { - adapter.updateItemDecoration() - adapter.statusActivity = data - } - - override fun onLoaderReset(loader: Loader) { - - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - val activity = activity ?: return - when (requestCode) { - REQUEST_SET_COLOR -> { - val status = adapter.status ?: return - if (resultCode == Activity.RESULT_OK) { - if (data == null) return - val color = data.getIntExtra(EXTRA_COLOR, Color.TRANSPARENT) - userColorNameManager.setUserColor(status.user_key, color) - } else if (resultCode == ColorPickerDialogActivity.RESULT_CLEARED) { - userColorNameManager.clearUserColor(status.user_key) - } - val args = arguments - if (args.containsKey(EXTRA_STATUS)) { - args.putParcelable(EXTRA_STATUS, status) - } - loaderManager.restartLoader(LOADER_ID_DETAIL_STATUS, args, this) - } - REQUEST_SELECT_ACCOUNT -> { - val status = adapter.status ?: return - if (resultCode == Activity.RESULT_OK) { - if (data == null || !data.hasExtra(EXTRA_ID)) return - val accountKey = data.getParcelableExtra(EXTRA_ACCOUNT_KEY) - IntentUtils.openStatus(activity, accountKey, status.id) - } - } - AbsStatusesFragment.REQUEST_FAVORITE_SELECT_ACCOUNT, - AbsStatusesFragment.REQUEST_RETWEET_SELECT_ACCOUNT -> { - AbsStatusesFragment.handleActionActivityResult(this, requestCode, resultCode, data) - } - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_status, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - setHasOptionsMenu(true) - Utils.setNdefPushMessageCallback(activity, CreateNdefMessageCallback { - val status = status ?: return@CreateNdefMessageCallback null - NdefMessage(arrayOf(NdefRecord.createUri(LinkCreator.getStatusWebLink(status)))) - }) - adapter = StatusAdapter(this) - layoutManager = StatusListLinearLayoutManager(context, recyclerView) - mItemDecoration = StatusDividerItemDecoration(context, adapter, layoutManager.orientation) - recyclerView.addItemDecoration(mItemDecoration) - layoutManager.recycleChildrenOnDetach = true - recyclerView.layoutManager = layoutManager - recyclerView.clipToPadding = false - adapter.statusClickListener = this - recyclerView.adapter = adapter - registerForContextMenu(recyclerView) - - scrollListener = RecyclerViewScrollHandler(this, RecyclerViewCallback(recyclerView)) - scrollListener.touchSlop = ViewConfiguration.get(context).scaledTouchSlop - - navigationHelper = RecyclerViewNavigationHelper(recyclerView, layoutManager, - adapter, null) - - setState(STATE_LOADING) - - loaderManager.initLoader(LOADER_ID_DETAIL_STATUS, arguments, this) - } - - override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) { - val status = adapter.getStatus(statusPosition) - IntentUtils.openMedia(activity, status, current, preferences[newDocumentApiKey], - preferences[displaySensitiveContentsKey]) - } - - override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) { - val status = adapter.getStatus(statusPosition) - val quotedMedia = status.quoted_media ?: return - IntentUtils.openMedia(activity, status.account_key, status.is_possibly_sensitive, status, - current, quotedMedia, preferences[newDocumentApiKey], - preferences[displaySensitiveContentsKey]) - } - - override fun onGapClick(holder: GapViewHolder, position: Int) { - - } - - override fun onItemActionClick(holder: ViewHolder, id: Int, position: Int) { - val status = adapter.getStatus(position) - handleActionClick(this@StatusFragment, id, status, holder as StatusViewHolder) - } - - - override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean { - val status = adapter.getStatus(position) - return AbsStatusesFragment.handleActionLongClick(this, status, adapter.getItemId(position), id) - } - - override fun onStatusClick(holder: IStatusViewHolder, position: Int) { - val status = adapter.getStatus(position) - IntentUtils.openStatus(activity, status) - } - - override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) { - val status = adapter.getStatus(position) - val quotedId = status.quoted_id ?: return - IntentUtils.openStatus(activity, status.account_key, quotedId) - } - - override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean { - return false - } - - override fun onItemMenuClick(holder: ViewHolder, menuView: View, position: Int) { - if (activity == null) return - val view = layoutManager.findViewByPosition(position) ?: return - recyclerView.showContextMenuForChild(view) - } - - override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) { - val status = adapter.getStatus(position) - IntentUtils.openUserProfile(activity, status.account_key, status.user_key, - status.user_screen_name, status.extras?.user_statusnet_profile_url, - preferences[newDocumentApiKey], null) - } - - override fun onMediaClick(view: View, current: ParcelableMedia, accountKey: UserKey?, id: Long) { - val status = adapter.status ?: return - if ((view.parent as View).id == R.id.quotedMediaPreview && status.quoted_media != null) { - IntentUtils.openMediaDirectly(activity, accountKey, status.quoted_media!!, current, - newDocument = preferences[newDocumentApiKey], status = status) - } else if (status.media != null) { - IntentUtils.openMediaDirectly(activity, accountKey, status.media!!, current, - newDocument = preferences[newDocumentApiKey], status = status) - } - } - - override fun handleKeyboardShortcutSingle(handler: KeyboardShortcutsHandler, - keyCode: Int, event: KeyEvent, - metaState: Int): Boolean { - if (!KeyboardShortcutsHandler.isValidForHotkey(keyCode, event)) return false - val focusedChild = RecyclerViewUtils.findRecyclerViewChild(recyclerView, layoutManager.focusedChild) - val position: Int - if (focusedChild != null && focusedChild.parent === recyclerView) { - position = recyclerView.getChildLayoutPosition(focusedChild) - } else { - return false - } - if (position == -1) return false - val status = adapter.getStatus(position) - val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: return false - return AbsStatusesFragment.handleKeyboardShortcutAction(this, action, status, position) - } - - override fun isKeyboardShortcutHandled(handler: KeyboardShortcutsHandler, keyCode: Int, event: KeyEvent, metaState: Int): Boolean { - val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: return false - when (action) { - ACTION_STATUS_REPLY, ACTION_STATUS_RETWEET, ACTION_STATUS_FAVORITE -> return true - } - return navigationHelper.isKeyboardShortcutHandled(handler, keyCode, event, metaState) - } - - override fun handleKeyboardShortcutRepeat(handler: KeyboardShortcutsHandler, - keyCode: Int, repeatCount: Int, - event: KeyEvent, metaState: Int): Boolean { - return navigationHelper.handleKeyboardShortcutRepeat(handler, keyCode, - repeatCount, event, metaState) - } - - override fun onCreateLoader(id: Int, args: Bundle): Loader> { - val fragmentArgs = arguments - val accountKey = fragmentArgs.getParcelable(EXTRA_ACCOUNT_KEY) - val statusId = fragmentArgs.getString(EXTRA_STATUS_ID) - return ParcelableStatusLoader(activity, false, fragmentArgs, accountKey, statusId) - } - - - override fun onLoadFinished(loader: Loader>, - data: SingleResponse) { - val activity = activity ?: return - val status = data.data - if (status != null) { - val readPosition = saveReadPosition() - val dataExtra = data.extras - val details: AccountDetails? = dataExtra.getParcelable(EXTRA_ACCOUNT) - if (adapter.setStatus(status, details)) { - val args = arguments - if (args.containsKey(EXTRA_STATUS)) { - args.putParcelable(EXTRA_STATUS, status) - } - adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.BOTH - adapter.setData(null) - loadConversation(status, null, null) - loadActivity(status) - - val position = adapter.getFirstPositionOfItem(StatusAdapter.ITEM_IDX_STATUS) - if (position != RecyclerView.NO_POSITION) { - layoutManager.scrollToPositionWithOffset(position, 0) - } - - Analyzer.log(StatusView(details?.type, status.media_type).apply { - this.type = StatusView.getStatusType(status) - this.source = status.source?.let(HtmlEscapeHelper::toPlainText) - }) - } else if (readPosition != null) { - restoreReadPosition(readPosition) - } - setState(STATE_LOADED) - } else { - adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.NONE - setState(STATE_ERROR) - val errorInfo = StatusCodeMessageUtils.getErrorInfo(context, data.exception!!) - errorText.spannable = errorInfo.message - errorIcon.setImageResource(errorInfo.icon) - } - activity.invalidateOptionsMenu() - } - - override fun onLoaderReset(loader: Loader>) { - } - - override val refreshing: Boolean - get() = loaderManager.hasRunningLoadersSafe() - - override fun onLoadMoreContents(@IndicatorPosition position: Long) { - if (!hasMoreConversation) return - if (ILoadMoreSupportAdapter.START in position) { - val start = adapter.getIndexStart(StatusAdapter.ITEM_IDX_CONVERSATION) - val first = adapter.getStatus(start, true) - if (first.in_reply_to_status_id == null) return - loadConversation(status, null, first.id) - } else if (ILoadMoreSupportAdapter.END in position) { - val start = adapter.getIndexStart(StatusAdapter.ITEM_IDX_CONVERSATION) - val last = adapter.getStatus(start + adapter.getStatusCount(true) - 1, true) - loadConversation(status, last.id, null) - } - adapter.loadMoreIndicatorPosition = position - } - - override fun setControlVisible(visible: Boolean) { - // No-op - } - - override fun onApplySystemWindowInsets(insets: Rect) { - recyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom) - } - - override val reachingEnd: Boolean - get() { - val lm = layoutManager - var itemPos = lm.findLastCompletelyVisibleItemPosition() - if (itemPos == RecyclerView.NO_POSITION) { - // No completely visible item, find visible item instead - itemPos = lm.findLastVisibleItemPosition() - } - return itemPos >= lm.itemCount - 1 - } - - override val reachingStart: Boolean - get() { - val lm = layoutManager - var itemPos = lm.findFirstCompletelyVisibleItemPosition() - if (itemPos == RecyclerView.NO_POSITION) { - // No completely visible item, find visible item instead - itemPos = lm.findFirstVisibleItemPosition() - } - return itemPos <= 1 - } - - private val status: ParcelableStatus? - get() = adapter.status - - private fun loadConversation(status: ParcelableStatus?, sinceId: String?, maxId: String?) { - if (status == null || activity == null) return - val args = Bundle { - this[EXTRA_ACCOUNT_KEY] = status.account_key - this[EXTRA_STATUS_ID] = status.originalId - this[EXTRA_SINCE_ID] = sinceId - this[EXTRA_MAX_ID] = maxId - this[EXTRA_STATUS] = status - } - if (conversationLoaderInitialized) { - loaderManager.restartLoader(LOADER_ID_STATUS_CONVERSATIONS, args, conversationsLoaderCallback) - return - } - loaderManager.initLoader(LOADER_ID_STATUS_CONVERSATIONS, args, conversationsLoaderCallback) - conversationLoaderInitialized = true - } - - - private fun loadActivity(status: ParcelableStatus?) { - if (status == null || host == null || isDetached) return - val args = Bundle { - this[EXTRA_ACCOUNT_KEY] = status.account_key - this[EXTRA_STATUS_ID] = status.originalId - } - if (activityLoaderInitialized) { - loaderManager.restartLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback) - return - } - loaderManager.initLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback) - activityLoaderInitialized = true - } - - private fun loadTranslation(status: ParcelableStatus?) { - if (status == null) return - if (loadTranslationTask?.isFinished ?: false) return - loadTranslationTask = run { - val task = LoadTranslationTask(this, status) - TaskStarter.execute(task) - return@run task - } - } - - private fun setConversation(data: List?) { - val readPosition = saveReadPosition() - val changed = adapter.setData(data) - hasMoreConversation = data != null && changed - restoreReadPosition(readPosition) - } - - private fun scrollToCurrent() { - if (adapter.status != null) { - val position = adapter.getFirstPositionOfItem(StatusAdapter.ITEM_IDX_STATUS) - recyclerView.smoothScrollToPosition(position) - } - } - - private fun displayTranslation(translation: TranslationResult) { - adapter.translationResult = translation - } - - private fun saveReadPosition(): ReadPosition? { - val lm = layoutManager - val adapter = this.adapter - val position = lm.findFirstVisibleItemPosition() - if (position == RecyclerView.NO_POSITION) return null - val itemType = adapter.getItemType(position) - var itemId = adapter.getItemId(position) - val positionView: View? - if (itemType == StatusAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) { - // Should be next item - positionView = lm.findViewByPosition(position + 1) - itemId = adapter.getItemId(position + 1) - } else { - positionView = lm.findViewByPosition(position) - } - return ReadPosition(itemId, positionView?.top ?: 0) - } - - private fun restoreReadPosition(position: ReadPosition?) { - val adapter = this.adapter - if (position == null) return - val adapterPosition = adapter.findPositionByItemId(position.statusId) - if (adapterPosition < 0) return - layoutManager.scrollToPositionWithOffset(adapterPosition, position.offsetTop) - } - - private fun setState(state: Int) { - statusContent.visibility = if (state == STATE_LOADED) View.VISIBLE else View.GONE - progressContainer.visibility = if (state == STATE_LOADING) View.VISIBLE else View.GONE - errorContainer.visibility = if (state == STATE_ERROR) View.VISIBLE else View.GONE - } - - override fun onStart() { - super.onStart() - bus.register(this) - recyclerView.addOnScrollListener(scrollListener) - recyclerView.setOnTouchListener(scrollListener.touchListener) - } - - override fun onStop() { - recyclerView.setOnTouchListener(null) - recyclerView.removeOnScrollListener(scrollListener) - bus.unregister(this) - super.onStop() - } - - override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { - if (!userVisibleHint) return - val contextMenuInfo = menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return - val status = adapter.getStatus(contextMenuInfo.position) - val inflater = MenuInflater(context) - inflater.inflate(R.menu.action_status, menu) - MenuUtils.setupForStatus(context, menu, preferences, twitterWrapper, userColorNameManager, - status) - } - - override fun onContextItemSelected(item: MenuItem): Boolean { - if (!userVisibleHint) return false - val contextMenuInfo = item.menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return false - val status = adapter.getStatus(contextMenuInfo.position) - if (item.itemId == R.id.share) { - val shareIntent = Utils.createStatusShareIntent(activity, status) - val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status)) - - startActivity(chooser) - - val am = AccountManager.get(context) - val accountType = AccountUtils.findByAccountKey(am, status.account_key)?.getAccountType(am) - - Analyzer.log(Share.status(accountType, status)) - return true - } - return MenuUtils.handleStatusClick(activity, this, fragmentManager, - preferences, userColorNameManager, twitterWrapper, status, item) - } - - @Subscribe - fun notifyStatusListChanged(event: StatusListChangedEvent) { - adapter.notifyDataSetChanged() - } - - @Subscribe - fun notifyFavoriteTask(event: FavoriteTaskEvent) { - if (!event.isSucceeded) return - val status = adapter.findStatusById(event.accountKey, event.statusId) - if (status != null) { - when (event.action) { - FavoriteTaskEvent.Action.CREATE -> { - status.is_favorite = true - } - FavoriteTaskEvent.Action.DESTROY -> { - status.is_favorite = false - } - } - } - } - - private fun onUserClick(user: ParcelableUser) { - IntentUtils.openUserProfile(context, user, true, null) - } - - class LoadSensitiveImageConfirmDialogFragment : BaseDialogFragment(), DialogInterface.OnClickListener { - - override fun onClick(dialog: DialogInterface, which: Int) { - when (which) { - DialogInterface.BUTTON_POSITIVE -> { - val f = parentFragment - if (f is StatusFragment) { - val adapter = f.adapter - adapter.isDetailMediaExpanded = true - } - } - } - - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val context = activity - val builder = AlertDialog.Builder(context) - builder.setTitle(android.R.string.dialog_alert_title) - builder.setMessage(R.string.sensitive_content_warning) - builder.setPositiveButton(android.R.string.ok, this) - builder.setNegativeButton(android.R.string.cancel, null) - val dialog = builder.create() - dialog.onShow { it.applyTheme() } - return dialog - } - } - - internal class LoadTranslationTask(fragment: StatusFragment, val status: ParcelableStatus) : - AbsAccountRequestTask(fragment.context, status.account_key) { - - val weakFragment = WeakReference(fragment) - - override fun onExecute(account: AccountDetails, params: Any?): TranslationResult { - val twitter = account.newMicroBlogInstance(context, MicroBlog::class.java) - val prefDest = preferences.getString(KEY_TRANSLATION_DESTINATION, null) - val dest: String - if (TextUtils.isEmpty(prefDest)) { - dest = twitter.accountSettings.language - val editor = preferences.edit() - editor.putString(KEY_TRANSLATION_DESTINATION, dest) - editor.apply() - } else { - dest = prefDest - } - return twitter.showTranslation(status.originalId, dest) - } - - override fun onSucceed(callback: Any?, result: TranslationResult) { - val fragment = weakFragment.get() ?: return - fragment.displayTranslation(result) - } - - override fun onException(callback: Any?, exception: MicroBlogException) { - Toast.makeText(context, exception.getErrorMessage(context), Toast.LENGTH_SHORT).show() - } - } - - private class DetailStatusViewHolder( - private val adapter: StatusAdapter, - itemView: View - ) : ViewHolder(itemView), OnClickListener, ActionMenuView.OnMenuItemClickListener { - - private val linkClickHandler: StatusLinkClickHandler - private val linkify: TwidereLinkify - - private val profileTypeView = itemView.profileType - private val nameView = itemView.name - private val summaryView = itemView.summary - private val textView = itemView.text - private val locationView = itemView.locationView - private val retweetedByView = itemView.retweetedBy - - init { - this.linkClickHandler = DetailStatusLinkClickHandler(adapter.context, - adapter.multiSelectManager, adapter, adapter.preferences) - this.linkify = TwidereLinkify(linkClickHandler) - - initViews() - } - - @UiThread - fun displayStatus(account: AccountDetails?, status: ParcelableStatus?, - statusActivity: StatusActivity?, translation: TranslationResult?) { - if (account == null || status == null) return - val fragment = adapter.fragment - val context = adapter.context - val formatter = adapter.bidiFormatter - val twitter = adapter.twitterWrapper - val nameFirst = adapter.nameFirst - val colorNameManager = adapter.userColorNameManager - - linkClickHandler.status = status - - if (status.retweet_id != null) { - val retweetedBy = colorNameManager.getDisplayName(status.retweeted_by_user_key!!, - status.retweeted_by_user_name!!, status.retweeted_by_user_acct!!, nameFirst) - retweetedByView.spannable = context.getString(R.string.name_retweeted, retweetedBy) - retweetedByView.visibility = View.VISIBLE - } else { - retweetedByView.spannable = null - retweetedByView.visibility = View.GONE - } - - itemView.profileContainer.drawEnd(status.account_color) - - val layoutPosition = layoutPosition - val skipLinksInText = status.extras?.support_entities ?: false - - if (status.is_quote) { - - itemView.quotedView.visibility = View.VISIBLE - - val quoteContentAvailable = status.quoted_text_plain != null && status.quoted_text_unescaped != null - - if (quoteContentAvailable) { - itemView.quotedName.visibility = View.VISIBLE - itemView.quotedText.visibility = View.VISIBLE - - itemView.quotedName.name = colorNameManager.getUserNickname(status.quoted_user_key!!, - status.quoted_user_name) - itemView.quotedName.screenName = "@${status.quoted_user_acct}" - itemView.quotedName.updateText(formatter) - - - val quotedDisplayEnd = status.extras?.quoted_display_text_range?.getOrNull(1) ?: -1 - val quotedText = SpannableStringBuilder.valueOf(status.quoted_text_unescaped) - status.quoted_spans?.applyTo(quotedText) - linkify.applyAllLinks(quotedText, status.account_key, layoutPosition.toLong(), - status.is_possibly_sensitive, skipLinksInText) - if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) { - itemView.quotedText.spannable = quotedText.subSequence(0, quotedDisplayEnd) - } else { - itemView.quotedText.spannable = quotedText - } - itemView.quotedText.hideIfEmpty() - - val quotedUserColor = colorNameManager.getUserColor(status.quoted_user_key!!) - if (quotedUserColor != 0) { - itemView.quotedView.drawStart(quotedUserColor) - } else { - itemView.quotedView.drawStart(ThemeUtils.getColorFromAttribute(context, - R.attr.quoteIndicatorBackgroundColor)) - } - - val quotedMedia = status.quoted_media - - if (quotedMedia?.isEmpty() ?: true) { - itemView.quotedMediaLabel.visibility = View.GONE - itemView.quotedMediaPreview.visibility = View.GONE - } else if (adapter.isDetailMediaExpanded) { - itemView.quotedMediaLabel.visibility = View.GONE - itemView.quotedMediaPreview.visibility = View.VISIBLE - itemView.quotedMediaPreview.displayMedia(adapter.requestManager, - media = quotedMedia, accountKey = status.account_key, - mediaClickListener = adapter.fragment) - } else { - itemView.quotedMediaLabel.visibility = View.VISIBLE - itemView.quotedMediaPreview.visibility = View.GONE - } - } else { - itemView.quotedName.visibility = View.GONE - itemView.quotedText.visibility = View.VISIBLE - itemView.quotedMediaLabel.visibility = View.GONE - itemView.quotedMediaPreview.visibility = View.GONE - - // Not available - val string = SpannableString.valueOf(context.getString(R.string.label_status_not_available)) - string.setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context, - android.R.attr.textColorTertiary, textView.currentTextColor)), 0, - string.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - itemView.quotedText.spannable = string - - itemView.quotedView.drawStart(ThemeUtils.getColorFromAttribute(context, - R.attr.quoteIndicatorBackgroundColor)) - } - } else { - itemView.quotedView.visibility = View.GONE - } - - itemView.profileContainer.drawStart(colorNameManager.getUserColor(status.user_key)) - - val timestamp: Long - - if (status.is_retweet) { - timestamp = status.retweet_timestamp - } else { - timestamp = status.timestamp - } - - nameView.name = colorNameManager.getUserNickname(status.user_key, status.user_name) - nameView.screenName = "@${status.user_acct}" - nameView.updateText(formatter) - - adapter.requestManager.loadProfileImage(context, status, adapter.profileImageStyle, - itemView.profileImage.cornerRadius, itemView.profileImage.cornerRadiusRatio, - size = ProfileImageSize.ORIGINAL).into(itemView.profileImage) - - val typeIconRes = Utils.getUserTypeIconRes(status.user_is_verified, status.user_is_protected) - val typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.user_is_verified, status.user_is_protected) - - - if (typeIconRes != 0 && typeDescriptionRes != 0) { - profileTypeView.setImageResource(typeIconRes) - profileTypeView.contentDescription = context.getString(typeDescriptionRes) - profileTypeView.visibility = View.VISIBLE - } else { - profileTypeView.setImageDrawable(null) - profileTypeView.contentDescription = null - profileTypeView.visibility = View.GONE - } - - val timeString = Utils.formatToLongTimeString(context, timestamp)?.takeIf(String::isNotEmpty) - val source = status.source?.takeIf(String::isNotEmpty) - itemView.timeSource.spannable = when { - timeString != null && source != null -> { - HtmlSpanBuilder.fromHtml(context.getString(R.string.status_format_time_source, - timeString, source)) - } - source != null -> HtmlSpanBuilder.fromHtml(source) - timeString != null -> timeString - else -> null - } - itemView.timeSource.movementMethod = LinkMovementMethod.getInstance() - - val displayEnd = status.extras?.display_text_range?.getOrNull(1) ?: -1 - val text = SpannableStringBuilder.valueOf(status.text_unescaped).apply { - status.spans?.applyTo(this) - linkify.applyAllLinks(this, status.account_key, layoutPosition.toLong(), - status.is_possibly_sensitive, skipLinksInText) - } - - summaryView.spannable = status.extras?.summary_text - summaryView.hideIfEmpty() - - if (displayEnd != -1 && displayEnd <= text.length) { - textView.spannable = text.subSequence(0, displayEnd) - } else { - textView.spannable = text - } - textView.hideIfEmpty() - - val location: ParcelableLocation? = status.location - val placeFullName: String? = status.place_full_name - - if (!TextUtils.isEmpty(placeFullName)) { - locationView.visibility = View.VISIBLE - locationView.spannable = placeFullName - locationView.isClickable = ParcelableLocationUtils.isValidLocation(location) - } else if (ParcelableLocationUtils.isValidLocation(location)) { - locationView.visibility = View.VISIBLE - locationView.setText(R.string.action_view_map) - locationView.isClickable = true - } else { - locationView.visibility = View.GONE - locationView.spannable = null - } - - val interactUsersAdapter = itemView.countsUsers.adapter as CountsUsersAdapter - if (statusActivity != null) { - updateStatusActivity(statusActivity) - } else { - interactUsersAdapter.setUsers(null) - interactUsersAdapter.setCounts(status) - } - - if (interactUsersAdapter.itemCount > 0) { - itemView.countsUsers.visibility = View.VISIBLE - itemView.countsUsersHeightHolder.visibility = View.INVISIBLE - } else { - itemView.countsUsers.visibility = View.GONE - itemView.countsUsersHeightHolder.visibility = View.GONE - } - - val media = status.media - - if (media?.isEmpty() ?: true) { - itemView.mediaPreviewContainer.visibility = View.GONE - itemView.mediaPreview.visibility = View.GONE - itemView.mediaPreviewLoad.visibility = View.GONE - itemView.mediaPreview.displayMedia() - } else if (adapter.isDetailMediaExpanded) { - itemView.mediaPreviewContainer.visibility = View.VISIBLE - itemView.mediaPreview.visibility = View.VISIBLE - itemView.mediaPreviewLoad.visibility = View.GONE - itemView.mediaPreview.displayMedia(adapter.requestManager, media = media, - accountKey = status.account_key, mediaClickListener = adapter.fragment) - } else { - itemView.mediaPreviewContainer.visibility = View.VISIBLE - itemView.mediaPreview.visibility = View.GONE - itemView.mediaPreviewLoad.visibility = View.VISIBLE - itemView.mediaPreview.displayMedia() - } - - if (TwitterCardUtils.isCardSupported(status)) { - val size = TwitterCardUtils.getCardSize(status.card!!) - - if (size != null) { - itemView.twitterCard.setCardSize(size.x, size.y) - } else { - itemView.twitterCard.setCardSize(0, 0) - } - val vc = TwitterCardViewFactory.from(status) - itemView.twitterCard.viewController = vc - if (vc != null) { - itemView.twitterCard.visibility = View.VISIBLE - } else { - itemView.twitterCard.visibility = View.GONE - } - - } else { - itemView.twitterCard.viewController = null - itemView.twitterCard.visibility = View.GONE - } - - MenuUtils.setupForStatus(context, itemView.menuBar.menu, fragment.preferences, twitter, - colorNameManager, status, adapter.statusAccount!!) - - - val lang = status.lang - if (CheckUtils.isValidLocale(lang) && account.isOfficial(context)) { - val locale = Locale(lang) - itemView.translateContainer.visibility = View.VISIBLE - if (translation != null) { - itemView.translateLabel.text = context.getString(R.string.label_translation) - itemView.translateResult.visibility = View.VISIBLE - itemView.translateResult.text = translation.text - } else { - itemView.translateLabel.text = context.getString(R.string.label_translate_from_language, - locale.displayLanguage) - itemView.translateResult.visibility = View.GONE - } - } else { - itemView.translateLabel.setText(R.string.unknown_language) - itemView.translateContainer.visibility = View.GONE - } - - textView.setTextIsSelectable(true) - itemView.translateResult.setTextIsSelectable(true) - - textView.movementMethod = LinkMovementMethod.getInstance() - itemView.quotedText.movementMethod = null - } - - override fun onClick(v: View) { - val status = adapter.getStatus(layoutPosition) - val fragment = adapter.fragment - val preferences = fragment.preferences - when (v) { - itemView.mediaPreviewLoad -> { - if (adapter.sensitiveContentEnabled || !status.is_possibly_sensitive) { - adapter.isDetailMediaExpanded = true - } else { - val f = LoadSensitiveImageConfirmDialogFragment() - f.show(fragment.childFragmentManager, "load_sensitive_image_confirm") - } - } - itemView.profileContainer -> { - val activity = fragment.activity - IntentUtils.openUserProfile(activity, status.account_key, status.user_key, - status.user_screen_name, status.extras?.user_statusnet_profile_url, - preferences[newDocumentApiKey], null) - } - retweetedByView -> { - if (status.retweet_id != null) { - IntentUtils.openUserProfile(adapter.context, status.account_key, - status.retweeted_by_user_key, status.retweeted_by_user_screen_name, - null, preferences[newDocumentApiKey], null) - } - } - locationView -> { - val location = status.location - if (!ParcelableLocationUtils.isValidLocation(location)) return - IntentUtils.openMap(adapter.context, location.latitude, location.longitude) - } - itemView.quotedView -> { - val quotedId = status.quoted_id ?: return - IntentUtils.openStatus(adapter.context, status.account_key, quotedId) - } - itemView.translateLabel -> { - fragment.loadTranslation(adapter.status) - } - } - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - val layoutPosition = layoutPosition - if (layoutPosition < 0) return false - val fragment = adapter.fragment - val status = adapter.getStatus(layoutPosition) - val preferences = fragment.preferences - val twitter = fragment.twitterWrapper - val manager = fragment.userColorNameManager - val activity = fragment.activity - return MenuUtils.handleStatusClick(activity, fragment, fragment.childFragmentManager, - preferences, manager, twitter, status, item) - } - - internal fun updateStatusActivity(activity: StatusActivity) { - val adapter = itemView.countsUsers.adapter as CountsUsersAdapter - adapter.setUsers(activity.retweeters) - adapter.setCounts(activity) - } - - private fun initViews() { - itemView.menuBar.setOnMenuItemClickListener(this) - val fragment = adapter.fragment - val activity = fragment.activity - val inflater = activity.menuInflater - val menu = itemView.menuBar.menu - inflater.inflate(R.menu.menu_detail_status, menu) - val favoriteItem = menu.findItem(R.id.favorite) - val provider = MenuItemCompat.getActionProvider(favoriteItem) - if (provider is FavoriteItemProvider) { - val defaultColor = ThemeUtils.getActionIconColor(activity) - provider.setDefaultColor(defaultColor) - val favoriteHighlight = ContextCompat.getColor(activity, R.color.highlight_favorite) - val likeHighlight = ContextCompat.getColor(activity, R.color.highlight_like) - val useStar = adapter.useStarsForLikes - provider.setActivatedColor(if (useStar) favoriteHighlight else likeHighlight) - provider.setIcon(if (useStar) R.drawable.ic_action_star else R.drawable.ic_action_heart) - provider.setUseStar(useStar) - provider.init(itemView.menuBar, favoriteItem) - } - ThemeUtils.wrapMenuIcon(itemView.menuBar, excludeGroups = MENU_GROUP_STATUS_SHARE) - itemView.mediaPreviewLoad.setOnClickListener(this) - itemView.profileContainer.setOnClickListener(this) - retweetedByView.setOnClickListener(this) - locationView.setOnClickListener(this) - itemView.quotedView.setOnClickListener(this) - itemView.translateLabel.setOnClickListener(this) - - val textSize = adapter.textSize - - nameView.setPrimaryTextSize(textSize * 1.25f) - nameView.setSecondaryTextSize(textSize * 0.85f) - summaryView.textSize = textSize * 1.25f - textView.textSize = textSize * 1.25f - - itemView.quotedName.setPrimaryTextSize(textSize * 1.25f) - itemView.quotedName.setSecondaryTextSize(textSize * 0.85f) - itemView.quotedText.textSize = textSize * 1.25f - - locationView.textSize = textSize * 0.85f - itemView.timeSource.textSize = textSize * 0.85f - itemView.translateLabel.textSize = textSize * 0.85f - itemView.translateResult.textSize = textSize * 1.05f - - itemView.countsUsersHeightHolder.count.textSize = textSize * 1.25f - itemView.countsUsersHeightHolder.label.textSize = textSize * 0.85f - - nameView.nameFirst = adapter.nameFirst - itemView.quotedName.nameFirst = adapter.nameFirst - - itemView.mediaPreview.style = adapter.mediaPreviewStyle - itemView.quotedMediaPreview.style = adapter.mediaPreviewStyle - - itemView.text.customSelectionActionModeCallback = StatusActionModeCallback(itemView.text, activity) - itemView.profileImage.style = adapter.profileImageStyle - - val layoutManager = LinearLayoutManager(adapter.context) - layoutManager.orientation = LinearLayoutManager.HORIZONTAL - itemView.countsUsers.layoutManager = layoutManager - - val countsUsersAdapter = CountsUsersAdapter(fragment, adapter) - itemView.countsUsers.adapter = countsUsersAdapter - val resources = activity.resources - itemView.countsUsers.addItemDecoration(SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.element_spacing_normal))) - - // Apply font families - nameView.applyFontFamily(adapter.lightFont) - summaryView.applyFontFamily(adapter.lightFont) - textView.applyFontFamily(adapter.lightFont) - itemView.quotedName.applyFontFamily(adapter.lightFont) - itemView.quotedText.applyFontFamily(adapter.lightFont) - itemView.locationView.applyFontFamily(adapter.lightFont) - itemView.translateLabel.applyFontFamily(adapter.lightFont) - itemView.translateResult.applyFontFamily(adapter.lightFont) - } - - - private class CountsUsersAdapter( - private val fragment: StatusFragment, - private val statusAdapter: StatusAdapter - ) : BaseRecyclerViewAdapter(statusAdapter.context, Glide.with(fragment)) { - - private val inflater = LayoutInflater.from(statusAdapter.context) - - private var counts: List? = null - private var users: List? = null - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - when (holder.itemViewType) { - ITEM_VIEW_TYPE_USER -> { - (holder as ProfileImageViewHolder).displayUser(getUser(position)!!) - } - ITEM_VIEW_TYPE_COUNT -> { - (holder as CountViewHolder).displayCount(getCount(position)!!) - } - } - } - - private fun getCount(position: Int): LabeledCount? { - if (counts == null) return null - if (position < countItemsCount) { - return counts!![position] - } - return null - } - - override fun getItemCount(): Int { - return countItemsCount + usersCount - } - - - override fun getItemViewType(position: Int): Int { - val countItemsCount = countItemsCount - if (position < countItemsCount) { - return ITEM_VIEW_TYPE_COUNT - } - return ITEM_VIEW_TYPE_USER - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - when (viewType) { - ITEM_VIEW_TYPE_USER -> return ProfileImageViewHolder(this, inflater.inflate(R.layout.adapter_item_status_interact_user, parent, false)) - ITEM_VIEW_TYPE_COUNT -> return CountViewHolder(this, inflater.inflate(R.layout.adapter_item_status_count_label, parent, false)) - } - throw UnsupportedOperationException("Unsupported viewType " + viewType) - } - - fun setUsers(users: List?) { - this.users = users - notifyDataSetChanged() - } - - - fun setCounts(activity: StatusActivity?) { - if (activity != null) { - val counts = ArrayList() - val replyCount = activity.replyCount - if (replyCount > 0) { - counts.add(LabeledCount(KEY_REPLY_COUNT, replyCount)) - } - val retweetCount = activity.retweetCount - if (retweetCount > 0) { - counts.add(LabeledCount(KEY_RETWEET_COUNT, retweetCount)) - } - val favoriteCount = activity.favoriteCount - if (favoriteCount > 0) { - counts.add(LabeledCount(KEY_FAVORITE_COUNT, favoriteCount)) - } - this.counts = counts - } else { - counts = null - } - notifyDataSetChanged() - } - - fun setCounts(status: ParcelableStatus?) { - if (status != null) { - val counts = ArrayList() - if (status.reply_count > 0) { - counts.add(LabeledCount(KEY_REPLY_COUNT, status.reply_count)) - } - if (status.retweet_count > 0) { - counts.add(LabeledCount(KEY_RETWEET_COUNT, status.retweet_count)) - } - if (status.favorite_count > 0) { - counts.add(LabeledCount(KEY_FAVORITE_COUNT, status.favorite_count)) - } - this.counts = counts - } else { - counts = null - } - notifyDataSetChanged() - } - - val countItemsCount: Int - get() { - if (counts == null) return 0 - return counts!!.size - } - - private val usersCount: Int - get() { - if (users == null) return 0 - return users!!.size - } - - private fun notifyItemClick(position: Int) { - when (getItemViewType(position)) { - ITEM_VIEW_TYPE_COUNT -> { - val count = getCount(position) - val status = statusAdapter.status - if (count == null || status == null) return - when (count.type) { - KEY_RETWEET_COUNT -> { - IntentUtils.openStatusRetweeters(context, status.account_key, - status.originalId) - } - KEY_FAVORITE_COUNT -> { - IntentUtils.openStatusFavoriters(context, status.account_key, - status.originalId) - } - } - } - ITEM_VIEW_TYPE_USER -> { - fragment.onUserClick(getUser(position)!!) - } - } - } - - private fun getUser(position: Int): ParcelableUser? { - val countItemsCount = countItemsCount - if (users == null || position < countItemsCount) return null - return users!![position - countItemsCount] - } - - - internal class ProfileImageViewHolder(private val adapter: CountsUsersAdapter, itemView: View) : ViewHolder(itemView), OnClickListener { - private val profileImageView = itemView.findViewById(R.id.profileImage) - - init { - itemView.setOnClickListener(this) - } - - fun displayUser(item: ParcelableUser) { - val context = adapter.context - val requestManager = adapter.requestManager - requestManager.loadProfileImage(context, item, adapter.profileImageStyle, - profileImageView.cornerRadius, profileImageView.cornerRadiusRatio, - adapter.profileImageSize).into(profileImageView) - } - - override fun onClick(v: View) { - adapter.notifyItemClick(layoutPosition) - } - } - - internal class CountViewHolder( - private val adapter: CountsUsersAdapter, - itemView: View - ) : ViewHolder(itemView), OnClickListener { - - init { - itemView.setOnClickListener(this) - val textSize = adapter.textSize - itemView.count.textSize = textSize * 1.25f - itemView.label.textSize = textSize * 0.85f - } - - override fun onClick(v: View) { - adapter.notifyItemClick(layoutPosition) - } - - fun displayCount(count: LabeledCount) { - val label: String - when (count.type) { - KEY_REPLY_COUNT -> { - label = adapter.context.getString(R.string.replies) - } - KEY_RETWEET_COUNT -> { - label = adapter.context.getString(R.string.count_label_retweets) - } - KEY_FAVORITE_COUNT -> { - label = adapter.context.getString(R.string.title_favorites) - } - else -> { - throw UnsupportedOperationException("Unsupported type " + count.type) - } - } - itemView.count.text = Utils.getLocalizedNumber(Locale.getDefault(), count.count) - itemView.label.text = label - } - } - - internal class LabeledCount(var type: Int, var count: Long) - - companion object { - private val ITEM_VIEW_TYPE_USER = 1 - private val ITEM_VIEW_TYPE_COUNT = 2 - - private val KEY_REPLY_COUNT = 1 - private val KEY_RETWEET_COUNT = 2 - private val KEY_FAVORITE_COUNT = 3 - } - } - - private class DetailStatusLinkClickHandler( - context: Context, - manager: MultiSelectManager, - private val adapter: StatusAdapter, - preferences: SharedPreferences - ) : StatusLinkClickHandler(context, manager, preferences) { - - override fun onLinkClick(link: String, orig: String?, accountKey: UserKey?, - extraId: Long, type: Int, sensitive: Boolean, start: Int, end: Int): Boolean { - val position = extraId.toInt() - val current = getCurrentMedia(link, position) - if (current != null && !current.open_browser) { - expandOrOpenMedia(current) - return true - } - return super.onLinkClick(link, orig, accountKey, extraId, type, sensitive, start, end) - } - - private fun expandOrOpenMedia(current: ParcelableMedia) { - if (adapter.isDetailMediaExpanded) { - IntentUtils.openMedia(adapter.context, adapter.status!!, current, - preferences[newDocumentApiKey], preferences[displaySensitiveContentsKey]) - return - } - adapter.isDetailMediaExpanded = true - } - - override fun isMedia(link: String, extraId: Long): Boolean { - val current = getCurrentMedia(link, extraId.toInt()) - return current != null && !current.open_browser - } - - private fun getCurrentMedia(link: String, extraId: Int): ParcelableMedia? { - val status = adapter.getStatus(extraId) - val media = ParcelableMediaUtils.getAllMedia(status) - return StatusLinkClickHandler.findByLink(media, link) - } - } - - private class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() { - - override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) { - if (ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL) { - outRect.set(spacing, 0, 0, 0) - } else { - outRect.set(0, 0, spacing, 0) - } - } - } - } - - private class SpaceViewHolder(itemView: View) : ViewHolder(itemView) - - class StatusAdapter( - val fragment: StatusFragment - ) : LoadMoreSupportAdapter(fragment.context, Glide.with(fragment)), - IStatusesAdapter>, IItemCountsAdapter { - - override val twidereLinkify: TwidereLinkify - - override var statusClickListener: StatusClickListener? = null - - override val itemCounts = ItemCounts(ITEM_TYPES_SUM) - - override val nameFirst = preferences[nameFirstKey] - override val mediaPreviewStyle = preferences[mediaPreviewStyleKey] - override val linkHighlightingStyle = preferences[linkHighlightOptionKey] - override val lightFont = preferences[lightFontKey] - override val mediaPreviewEnabled = preferences[mediaPreviewKey] - override val sensitiveContentEnabled = preferences[displaySensitiveContentsKey] - override val useStarsForLikes = preferences[iWantMyStarsBackKey] - - private val inflater: LayoutInflater - private val cardBackgroundColor: Int - private val showCardActions = !preferences[hideCardActionsKey] - private var recyclerView: RecyclerView? = null - private var detailMediaExpanded: Boolean = false - - var status: ParcelableStatus? = null - internal set - var translationResult: TranslationResult? = null - internal set(translation) { - if (translation == null || status?.originalId != translation.id) { - field = null - } else { - field = translation - } - notifyDataSetChanged() - } - var statusActivity: StatusActivity? = null - internal set(value) { - val status = status ?: return - if (value != null && !value.isStatus(status)) { - return - } - field = value - val statusIndex = getIndexStart(ITEM_IDX_STATUS) - notifyItemChanged(statusIndex, value) - } - var statusAccount: AccountDetails? = null - internal set - - private var data: List? = null - private var replyError: CharSequence? = null - private var conversationError: CharSequence? = null - private var replyStart: Int = 0 - private var showingActionCardPosition = RecyclerView.NO_POSITION - private val showingFullTextStates = SparseBooleanArray() - - init { - setHasStableIds(true) - val context = fragment.activity - // There's always a space at the end of the list - itemCounts[ITEM_IDX_SPACE] = 1 - itemCounts[ITEM_IDX_STATUS] = 1 - itemCounts[ITEM_IDX_CONVERSATION_LOAD_MORE] = 1 - itemCounts[ITEM_IDX_REPLY_LOAD_MORE] = 1 - inflater = LayoutInflater.from(context) - cardBackgroundColor = ThemeUtils.getCardBackgroundColor(context, - preferences[themeBackgroundOptionKey], preferences[themeBackgroundAlphaKey]) - val listener = StatusAdapterLinkClickHandler>(context, preferences) - listener.setAdapter(this) - twidereLinkify = TwidereLinkify(listener) - } - - override fun getStatus(position: Int, raw: Boolean): ParcelableStatus { - when (getItemCountIndex(position, raw)) { - ITEM_IDX_CONVERSATION -> { - var idx = position - getIndexStart(ITEM_IDX_CONVERSATION) - if (data!![idx].is_filtered) idx++ - return data!![idx] - } - ITEM_IDX_REPLY -> { - var idx = position - getIndexStart(ITEM_IDX_CONVERSATION) - - getTypeCount(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_STATUS) + - replyStart - if (data!![idx].is_filtered) idx++ - return data!![idx] - } - ITEM_IDX_STATUS -> { - return status!! - } - } - throw IndexOutOfBoundsException("index: $position") - } - - fun getIndexStart(index: Int): Int { - if (index == 0) return 0 - return itemCounts.getItemStartPosition(index) - } - - override fun getStatusId(position: Int, raw: Boolean): String { - return getStatus(position, raw).id - } - - override fun getStatusTimestamp(position: Int, raw: Boolean): Long { - return getStatus(position, raw).timestamp - } - - override fun getStatusPositionKey(position: Int, raw: Boolean): Long { - val status = getStatus(position, raw) - return if (status.position_key > 0) status.timestamp else getStatusTimestamp(position, raw) - } - - override fun getAccountKey(position: Int, raw: Boolean) = getStatus(position, raw).account_key - - override fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? { - if (status != null && accountKey == status!!.account_key && TextUtils.equals(statusId, status!!.id)) { - return status - } - return data?.firstOrNull { accountKey == it.account_key && TextUtils.equals(it.id, statusId) } - } - - override fun getStatusCount(raw: Boolean): Int { - return getTypeCount(ITEM_IDX_CONVERSATION) + getTypeCount(ITEM_IDX_STATUS) + getTypeCount(ITEM_IDX_REPLY) - } - - override fun isCardActionsShown(position: Int): Boolean { - if (position == RecyclerView.NO_POSITION) return showCardActions - return showCardActions || showingActionCardPosition == position - } - - override fun showCardActions(position: Int) { - if (showingActionCardPosition != RecyclerView.NO_POSITION) { - notifyItemChanged(showingActionCardPosition) - } - showingActionCardPosition = position - if (position != RecyclerView.NO_POSITION) { - notifyItemChanged(position) - } - } - - override fun isFullTextVisible(position: Int): Boolean { - return showingFullTextStates.get(position) - } - - override fun setFullTextVisible(position: Int, visible: Boolean) { - showingFullTextStates.put(position, visible) - if (position != RecyclerView.NO_POSITION) { - notifyItemChanged(position) - } - } - override fun setData(data: List?): Boolean { - val status = this.status ?: return false - val changed = this.data != data - this.data = data - if (data == null || data.isEmpty()) { - setTypeCount(ITEM_IDX_CONVERSATION, 0) - setTypeCount(ITEM_IDX_REPLY, 0) - replyStart = -1 - } else { - var sortId = status.sort_id - - if (status.is_retweet) { - sortId = data.find { - it.id == status.retweet_id - }?.sort_id ?: status.retweet_timestamp - } - var conversationCount = 0 - var replyCount = 0 - var replyStart = -1 - data.forEachIndexed { i, item -> - if (item.sort_id < sortId) { - if (!item.is_filtered) { - conversationCount++ - } - } else if (status.id == item.id) { - this.status = item - } else if (item.sort_id > sortId) { - if (replyStart < 0) { - replyStart = i - } - if (!item.is_filtered) { - replyCount++ - } - } - } - setTypeCount(ITEM_IDX_CONVERSATION, conversationCount) - setTypeCount(ITEM_IDX_REPLY, replyCount) - this.replyStart = replyStart - } - notifyDataSetChanged() - updateItemDecoration() - return changed - } - - override val showAccountsColor: Boolean - get() = false - - var isDetailMediaExpanded: Boolean - get() { - if (detailMediaExpanded) return true - if (mediaPreviewEnabled) { - val status = this.status - return status != null && (sensitiveContentEnabled || !status.is_possibly_sensitive) - } - return false - } - set(expanded) { - detailMediaExpanded = expanded - notifyDataSetChanged() - updateItemDecoration() - } - - override fun isGapItem(position: Int): Boolean { - return false - } - - override val gapClickListener: IGapSupportedAdapter.GapClickListener? - get() = statusClickListener - - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder? { - when (viewType) { - VIEW_TYPE_DETAIL_STATUS -> { - val view = inflater.inflate(R.layout.header_status, parent, false) - view.setBackgroundColor(cardBackgroundColor) - return DetailStatusViewHolder(this, view) - } - VIEW_TYPE_LIST_STATUS -> { - return ListParcelableStatusesAdapter.createStatusViewHolder(this, inflater, parent) - } - VIEW_TYPE_CONVERSATION_LOAD_INDICATOR, VIEW_TYPE_REPLIES_LOAD_INDICATOR -> { - val view = inflater.inflate(R.layout.list_item_load_indicator, parent, - false) - return LoadIndicatorViewHolder(view) - } - VIEW_TYPE_SPACE -> { - return SpaceViewHolder(Space(context)) - } - VIEW_TYPE_REPLY_ERROR -> { - val view = inflater.inflate(R.layout.adapter_item_status_error, parent, - false) - return StatusErrorItemViewHolder(view) - } - } - return null - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List) { - var handled = false - when (holder.itemViewType) { - VIEW_TYPE_DETAIL_STATUS -> { - holder as DetailStatusViewHolder - payloads.forEach { it -> - when (it) { - is StatusActivity -> { - holder.updateStatusActivity(it) - } - is ParcelableStatus -> { - holder.displayStatus(statusAccount, status, statusActivity, - translationResult) - } - } - handled = true - } - } - } - if (handled) return - super.onBindViewHolder(holder, position, payloads) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - when (holder.itemViewType) { - VIEW_TYPE_DETAIL_STATUS -> { - val status = getStatus(position) - val detailHolder = holder as DetailStatusViewHolder - detailHolder.displayStatus(statusAccount, status, statusActivity, translationResult) - } - VIEW_TYPE_LIST_STATUS -> { - val status = getStatus(position) - val statusHolder = holder as IStatusViewHolder - // Display 'in reply to' for first item - // useful to indicate whether first tweet has reply or not - // We only display that indicator for first conversation item - val itemType = getItemType(position) - val displayInReplyTo = itemType == ITEM_IDX_CONVERSATION && position - getItemTypeStart(position) == 0 - statusHolder.display(status = status, displayInReplyTo = displayInReplyTo) - } - VIEW_TYPE_REPLY_ERROR -> { - val errorHolder = holder as StatusErrorItemViewHolder - errorHolder.showError(replyError!!) - } - VIEW_TYPE_CONVERSATION_ERROR -> { - val errorHolder = holder as StatusErrorItemViewHolder - errorHolder.showError(conversationError!!) - } - VIEW_TYPE_CONVERSATION_LOAD_INDICATOR -> { - val indicatorHolder = holder as LoadIndicatorViewHolder - indicatorHolder.setLoadProgressVisible(isConversationsLoading) - } - VIEW_TYPE_REPLIES_LOAD_INDICATOR -> { - val indicatorHolder = holder as LoadIndicatorViewHolder - indicatorHolder.setLoadProgressVisible(isRepliesLoading) - } - } - } - - override fun getItemViewType(position: Int): Int { - return getItemViewTypeByItemType(getItemType(position)) - } - - override fun addGapLoadingId(id: ObjectId) { - - } - - override fun removeGapLoadingId(id: ObjectId) { - - } - - private fun getItemViewTypeByItemType(type: Int): Int { - when (type) { - ITEM_IDX_CONVERSATION, ITEM_IDX_REPLY -> return VIEW_TYPE_LIST_STATUS - ITEM_IDX_CONVERSATION_LOAD_MORE -> return VIEW_TYPE_CONVERSATION_LOAD_INDICATOR - ITEM_IDX_REPLY_LOAD_MORE -> return VIEW_TYPE_REPLIES_LOAD_INDICATOR - ITEM_IDX_STATUS -> return VIEW_TYPE_DETAIL_STATUS - ITEM_IDX_SPACE -> return VIEW_TYPE_SPACE - ITEM_IDX_REPLY_ERROR -> return VIEW_TYPE_REPLY_ERROR - ITEM_IDX_CONVERSATION_ERROR -> return VIEW_TYPE_CONVERSATION_ERROR - } - throw IllegalStateException() - } - - private fun getItemCountIndex(position: Int, raw: Boolean): Int { - return itemCounts.getItemCountIndex(position) - } - - fun getItemType(position: Int): Int { - var typeStart = 0 - for (type in 0 until ITEM_TYPES_SUM) { - val typeCount = getTypeCount(type) - val typeEnd = typeStart + typeCount - if (position in typeStart until typeEnd) return type - typeStart = typeEnd - } - throw IllegalStateException("Unknown position " + position) - } - - fun getItemTypeStart(position: Int): Int { - var typeStart = 0 - for (type in 0 until ITEM_TYPES_SUM) { - val typeCount = getTypeCount(type) - val typeEnd = typeStart + typeCount - if (position in typeStart until typeEnd) return typeStart - typeStart = typeEnd - } - throw IllegalStateException() - } - - override fun getItemId(position: Int): Long { - val countIndex = getItemCountIndex(position) - when (countIndex) { - ITEM_IDX_CONVERSATION, ITEM_IDX_STATUS, ITEM_IDX_REPLY -> { - val status = getStatus(position) - val hashCode = ParcelableStatus.calculateHashCode(status.account_key, status.id) - return (countIndex.toLong() shl 32) or hashCode.toLong() - } - } - val countPos = (position - getItemStartPosition(countIndex)).toLong() - return (countIndex.toLong() shl 32) or countPos - } - - override fun getItemCount(): Int { - if (status == null) return 0 - return itemCounts.itemCount - } - - override fun onAttachedToRecyclerView(recyclerView: RecyclerView?) { - super.onAttachedToRecyclerView(recyclerView) - this.recyclerView = recyclerView - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView?) { - super.onDetachedFromRecyclerView(recyclerView) - this.recyclerView = null - } - - private fun setTypeCount(idx: Int, size: Int) { - itemCounts[idx] = size - notifyDataSetChanged() - } - - fun getTypeCount(idx: Int): Int { - return itemCounts[idx] - } - - fun setReplyError(error: CharSequence?) { - replyError = error - setTypeCount(ITEM_IDX_REPLY_ERROR, if (error != null) 1 else 0) - updateItemDecoration() - } - - fun setConversationError(error: CharSequence?) { - conversationError = error - setTypeCount(ITEM_IDX_CONVERSATION_ERROR, if (error != null) 1 else 0) - updateItemDecoration() - } - - fun setStatus(status: ParcelableStatus, account: AccountDetails?): Boolean { - val oldStatus = this.status - val oldAccount = this.statusAccount - val changed = oldStatus != status && oldAccount != account - this.status = status - this.statusAccount = account - if (changed) { - notifyDataSetChanged() - updateItemDecoration() - } else { - val statusIndex = getIndexStart(ITEM_IDX_STATUS) - notifyItemChanged(statusIndex, status) - } - return changed - } - - fun updateItemDecoration() { - if (recyclerView == null) return - } - - fun getFirstPositionOfItem(itemIdx: Int): Int { - var position = 0 - for (i in 0 until ITEM_TYPES_SUM) { - if (itemIdx == i) return position - position += getTypeCount(i) - } - return RecyclerView.NO_POSITION - } - - - fun getData(): List? { - return data - } - - var isConversationsLoading: Boolean - get() = ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition - set(loading) { - if (loading) { - loadMoreIndicatorPosition = loadMoreIndicatorPosition or ILoadMoreSupportAdapter.START - } else { - loadMoreIndicatorPosition = loadMoreIndicatorPosition and ILoadMoreSupportAdapter.START.inv() - } - updateItemDecoration() - } - - var isRepliesLoading: Boolean - get() = ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition - set(loading) { - if (loading) { - loadMoreIndicatorPosition = loadMoreIndicatorPosition or ILoadMoreSupportAdapter.END - } else { - loadMoreIndicatorPosition = loadMoreIndicatorPosition and ILoadMoreSupportAdapter.END.inv() - } - updateItemDecoration() - } - - class StatusErrorItemViewHolder(itemView: View) : ViewHolder(itemView) { - private val textView = itemView.findViewById(android.R.id.text1) - - init { - textView.movementMethod = LinkMovementMethod.getInstance() - textView.linksClickable = true - } - - fun showError(text: CharSequence) { - textView.text = text - } - } - - companion object { - - const val VIEW_TYPE_LIST_STATUS = 0 - const val VIEW_TYPE_DETAIL_STATUS = 1 - const val VIEW_TYPE_CONVERSATION_LOAD_INDICATOR = 2 - const val VIEW_TYPE_REPLIES_LOAD_INDICATOR = 3 - const val VIEW_TYPE_REPLY_ERROR = 4 - const val VIEW_TYPE_CONVERSATION_ERROR = 5 - const val VIEW_TYPE_SPACE = 6 - const val VIEW_TYPE_EMPTY = 7 - - const val ITEM_IDX_CONVERSATION_LOAD_MORE = 0 - const val ITEM_IDX_CONVERSATION_ERROR = 1 - const val ITEM_IDX_CONVERSATION = 2 - const val ITEM_IDX_STATUS = 3 - const val ITEM_IDX_REPLY = 4 - const val ITEM_IDX_REPLY_ERROR = 5 - const val ITEM_IDX_REPLY_LOAD_MORE = 6 - const val ITEM_IDX_SPACE = 7 - const val ITEM_TYPES_SUM = 8 - } - } - - private class StatusListLinearLayoutManager(context: Context, private val recyclerView: RecyclerView) : FixedLinearLayoutManager(context) { - private var spaceHeight: Int = 0 - - init { - orientation = LinearLayoutManager.VERTICAL - } - - override fun getDecoratedMeasuredHeight(child: View): Int { - if (getItemViewType(child) == StatusAdapter.VIEW_TYPE_SPACE) { - val height = calculateSpaceItemHeight(child, StatusAdapter.VIEW_TYPE_SPACE, - StatusAdapter.VIEW_TYPE_DETAIL_STATUS) - if (height >= 0) { - return height - } - } - return super.getDecoratedMeasuredHeight(child) - } - - override fun setOrientation(orientation: Int) { - if (orientation != LinearLayoutManager.VERTICAL) - throw IllegalArgumentException("Only VERTICAL orientation supported") - super.setOrientation(orientation) - } - - - override fun computeVerticalScrollExtent(state: RecyclerView.State?): Int { - val firstPosition = findFirstVisibleItemPosition() - val lastPosition = Math.min(validScrollItemCount - 1, findLastVisibleItemPosition()) - if (firstPosition < 0 || lastPosition < 0) return 0 - val childCount = lastPosition - firstPosition + 1 - if (childCount > 0) { - if (isSmoothScrollbarEnabled) { - var extent = childCount * 100 - var view = findViewByPosition(firstPosition) ?: return 0 - val top = view.top - var height = view.height - if (height > 0) { - extent += top * 100 / height - } - - view = findViewByPosition(lastPosition) ?: return 0 - val bottom = view.bottom - height = view.height - if (height > 0) { - extent -= (bottom - getHeight()) * 100 / height - } - return extent - } else { - return 1 - } - } - return 0 - } - - override fun computeVerticalScrollOffset(state: RecyclerView.State?): Int { - val firstPosition = findFirstVisibleItemPosition() - val lastPosition = Math.min(validScrollItemCount - 1, findLastVisibleItemPosition()) - if (firstPosition < 0 || lastPosition < 0) return 0 - val childCount = lastPosition - firstPosition + 1 - val skippedCount = skippedScrollItemCount - if (firstPosition >= skippedCount && childCount > 0) { - if (isSmoothScrollbarEnabled) { - val view = findViewByPosition(firstPosition) ?: return 0 - val top = view.top - val height = view.height - if (height > 0) { - return Math.max((firstPosition - skippedCount) * 100 - top * 100 / height, 0) - } - } else { - val index: Int - val count = validScrollItemCount - if (firstPosition == 0) { - index = 0 - } else if (firstPosition + childCount == count) { - index = count - } else { - index = firstPosition + childCount / 2 - } - return (firstPosition + childCount * (index / count.toFloat())).toInt() - } - } - return 0 - } - - override fun computeVerticalScrollRange(state: RecyclerView.State?): Int { - val result: Int - if (isSmoothScrollbarEnabled) { - result = Math.max(validScrollItemCount * 100, 0) - } else { - result = validScrollItemCount - } - return result - } - - private val skippedScrollItemCount: Int - get() { - val adapter = recyclerView.adapter as StatusAdapter - var skipped = 0 - if (!adapter.isConversationsLoading) { - skipped += adapter.getTypeCount(StatusAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) - } - return skipped - } - - private val validScrollItemCount: Int - get() { - val adapter = recyclerView.adapter as StatusAdapter - var count = 0 - if (adapter.isConversationsLoading) { - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) - } - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_CONVERSATION_ERROR) - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_CONVERSATION) - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_STATUS) - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_REPLY) - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_REPLY_ERROR) - if (adapter.isRepliesLoading) { - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_REPLY_LOAD_MORE) - } - val spaceHeight = calculateSpaceHeight() - if (spaceHeight > 0) { - count += adapter.getTypeCount(StatusAdapter.ITEM_IDX_SPACE) - } - return count - } - - private fun calculateSpaceHeight(): Int { - val space = findViewByPosition(itemCount - 1) ?: return spaceHeight - spaceHeight = getDecoratedMeasuredHeight(space) - return spaceHeight - } - - - } - - class StatusActivitySummaryLoader( - context: Context, - private val accountKey: UserKey, - private val statusId: String - ) : FixedAsyncTaskLoader(context) { - - override fun loadInBackground(): StatusActivity? { - val context = context - val details = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true) ?: return null - if (AccountType.TWITTER != details.type) { - return null - } - val twitter = MicroBlogAPIFactory.getInstance(context, accountKey) ?: return null - val paging = Paging() - paging.setCount(10) - val activitySummary = StatusActivity(statusId, emptyList()) - try { - activitySummary.retweeters = twitter.getRetweets(statusId, paging) - .filterNot { DataStoreUtils.isFilteringUser(context, it.user.key) } - .distinctBy { it.user.id } - .map { it.user.toParcelable(details) } - val countValues = ContentValues() - val status = twitter.showStatus(statusId) - activitySummary.favoriteCount = status.favoriteCount - activitySummary.retweetCount = status.retweetCount - activitySummary.replyCount = status.replyCount - - countValues.put(Statuses.REPLY_COUNT, activitySummary.replyCount) - countValues.put(Statuses.FAVORITE_COUNT, activitySummary.favoriteCount) - countValues.put(Statuses.RETWEET_COUNT, activitySummary.retweetCount) - - val cr = context.contentResolver - val statusWhere = Expression.and( - Expression.equalsArgs(Statuses.ACCOUNT_KEY), - Expression.or( - Expression.equalsArgs(Statuses.ID), - Expression.equalsArgs(Statuses.RETWEET_ID))) - val statusWhereArgs = arrayOf(accountKey.toString(), statusId, statusId) - cr.update(Statuses.CONTENT_URI, countValues, statusWhere.sql, statusWhereArgs) - cr.updateStatusInfo(DataStoreUtils.STATUSES_ACTIVITIES_URIS, Statuses.COLUMNS, - accountKey, statusId, ParcelableStatus::class.java) { item -> - item.favorite_count = activitySummary.favoriteCount - item.reply_count = activitySummary.replyCount - item.retweet_count = activitySummary.retweetCount - return@updateStatusInfo item - } - val pStatus = status.toParcelable(details) - cr.insert(CachedStatuses.CONTENT_URI, ObjectCursor - .valuesCreatorFrom(ParcelableStatus::class.java).create(pStatus)) - - return activitySummary - } catch (e: MicroBlogException) { - return null - } - - } - - override fun onStartLoading() { - forceLoad() - } - } - - data class StatusActivity( - var statusId: String, - var retweeters: List, - var favoriteCount: Long = 0, - var replyCount: Long = -1, - var retweetCount: Long = 0 - ) { - - fun isStatus(status: ParcelableStatus): Boolean { - return statusId == status.retweet_id ?: status.id - } - } - - data class ReadPosition(var statusId: Long, var offsetTop: Int) - - private class StatusDividerItemDecoration( - context: Context, - private val statusAdapter: StatusAdapter, - orientation: Int - ) : ExtendedDividerItemDecoration(context, orientation) { - - override fun isDividerEnabled(childPos: Int): Boolean { - if (childPos >= statusAdapter.itemCount || childPos < 0) return false - val itemType = statusAdapter.getItemType(childPos) - when (itemType) { - StatusAdapter.ITEM_IDX_REPLY_LOAD_MORE, StatusAdapter.ITEM_IDX_REPLY_ERROR, - StatusAdapter.ITEM_IDX_SPACE -> return false - } - return true - } - - } - - companion object { - - // Constants - private val LOADER_ID_DETAIL_STATUS = 1 - private val LOADER_ID_STATUS_CONVERSATIONS = 2 - private val LOADER_ID_STATUS_ACTIVITY = 3 - private val STATE_LOADED = 1 - private val STATE_LOADING = 2 - private val STATE_ERROR = 3 - - fun Bundle.toPagination(): Pagination { - val maxId = getString(EXTRA_MAX_ID) - val sinceId = getString(EXTRA_SINCE_ID) - val maxSortId = getLong(EXTRA_MAX_SORT_ID) - val sinceSortId = getLong(EXTRA_SINCE_SORT_ID) - return SinceMaxPagination().apply { - this.maxId = maxId - this.sinceId = sinceId - this.maxSortId = maxSortId - this.sinceSortId = sinceSortId - } - } - - } -} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt new file mode 100644 index 000000000..7adf28335 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt @@ -0,0 +1,960 @@ +/* + * 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.fragment.status + +import android.accounts.AccountManager +import android.app.Activity +import android.app.Dialog +import android.content.ContentValues +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.graphics.Color +import android.graphics.Rect +import android.nfc.NdefMessage +import android.nfc.NdefRecord +import android.nfc.NfcAdapter.CreateNdefMessageCallback +import android.os.Bundle +import android.support.v4.app.LoaderManager.LoaderCallbacks +import android.support.v4.app.hasRunningLoadersSafe +import android.support.v4.content.FixedAsyncTaskLoader +import android.support.v4.content.Loader +import android.support.v7.app.AlertDialog +import android.support.v7.widget.FixedLinearLayoutManager +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.RecyclerView.ViewHolder +import android.text.TextUtils +import android.view.* +import android.widget.Toast +import com.squareup.otto.Subscribe +import kotlinx.android.synthetic.main.fragment_status.* +import kotlinx.android.synthetic.main.layout_content_fragment_common.* +import nl.komponents.kovenant.combine.and +import nl.komponents.kovenant.task +import nl.komponents.kovenant.ui.alwaysUi +import nl.komponents.kovenant.ui.successUi +import org.mariotaku.abstask.library.TaskStarter +import org.mariotaku.kpreferences.get +import org.mariotaku.ktextension.* +import org.mariotaku.library.objectcursor.ObjectCursor +import org.mariotaku.microblog.library.MicroBlog +import org.mariotaku.microblog.library.MicroBlogException +import org.mariotaku.microblog.library.twitter.model.Paging +import org.mariotaku.microblog.library.twitter.model.TranslationResult +import org.mariotaku.sqliteqb.library.Expression +import org.mariotaku.twidere.Constants.* +import org.mariotaku.twidere.R +import org.mariotaku.twidere.activity.ColorPickerDialogActivity +import org.mariotaku.twidere.adapter.StatusDetailsAdapter +import org.mariotaku.twidere.adapter.decorator.ExtendedDividerItemDecoration +import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter +import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition +import org.mariotaku.twidere.annotation.AccountType +import org.mariotaku.twidere.constant.KeyboardShortcutConstants.* +import org.mariotaku.twidere.constant.displaySensitiveContentsKey +import org.mariotaku.twidere.constant.newDocumentApiKey +import org.mariotaku.twidere.extension.* +import org.mariotaku.twidere.extension.model.api.key +import org.mariotaku.twidere.extension.model.api.toParcelable +import org.mariotaku.twidere.extension.model.getAccountType +import org.mariotaku.twidere.extension.model.media_type +import org.mariotaku.twidere.extension.model.newMicroBlogInstance +import org.mariotaku.twidere.extension.model.originalId +import org.mariotaku.twidere.extension.view.calculateSpaceItemHeight +import org.mariotaku.twidere.fragment.AbsStatusesFragment +import org.mariotaku.twidere.fragment.AbsStatusesFragment.Companion.handleActionClick +import org.mariotaku.twidere.fragment.BaseDialogFragment +import org.mariotaku.twidere.fragment.BaseFragment +import org.mariotaku.twidere.fragment.status.TranslationDestinationDialogFragment +import org.mariotaku.twidere.loader.ParcelableStatusLoader +import org.mariotaku.twidere.loader.statuses.ConversationLoader +import org.mariotaku.twidere.model.* +import org.mariotaku.twidere.model.analyzer.Share +import org.mariotaku.twidere.model.analyzer.StatusView +import org.mariotaku.twidere.model.event.FavoriteTaskEvent +import org.mariotaku.twidere.model.event.StatusListChangedEvent +import org.mariotaku.twidere.model.pagination.Pagination +import org.mariotaku.twidere.model.pagination.SinceMaxPagination +import org.mariotaku.twidere.model.util.AccountUtils +import org.mariotaku.twidere.provider.TwidereDataStore.CachedStatuses +import org.mariotaku.twidere.provider.TwidereDataStore.Statuses +import org.mariotaku.twidere.task.AbsAccountRequestTask +import org.mariotaku.twidere.util.* +import org.mariotaku.twidere.util.ContentScrollHandler.ContentListSupport +import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback +import org.mariotaku.twidere.util.RecyclerViewScrollHandler.RecyclerViewCallback +import org.mariotaku.twidere.view.CardMediaContainer.OnMediaClickListener +import org.mariotaku.twidere.view.ExtendedRecyclerView +import org.mariotaku.twidere.view.holder.GapViewHolder +import org.mariotaku.twidere.view.holder.StatusViewHolder +import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder +import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder.StatusClickListener +import java.lang.ref.WeakReference + +/** + * Displays status details + * Created by mariotaku on 14/12/5. + */ +class StatusFragment : BaseFragment(), LoaderCallbacks>, + OnMediaClickListener, StatusClickListener, KeyboardShortcutCallback, + ContentListSupport { + private var mItemDecoration: ExtendedDividerItemDecoration? = null + + override lateinit var adapter: StatusDetailsAdapter + + private lateinit var layoutManager: LinearLayoutManager + private lateinit var navigationHelper: RecyclerViewNavigationHelper + private lateinit var scrollListener: RecyclerViewScrollHandler + + private var loadTranslationTask: LoadTranslationTask? = null + // Data fields + private var conversationLoaderInitialized: Boolean = false + + private var activityLoaderInitialized: Boolean = false + private var hasMoreConversation = true + + // Listeners + private val conversationsLoaderCallback = object : LoaderCallbacks> { + override fun onCreateLoader(id: Int, args: Bundle): Loader> { + val adapter = this@StatusFragment.adapter + adapter.isRepliesLoading = true + adapter.isConversationsLoading = true + adapter.updateItemDecoration() + val status: ParcelableStatus = args.getParcelable(EXTRA_STATUS) + val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false) + return ConversationLoader(activity, status, adapter.getData(), true, loadingMore).apply { + pagination = args.toPagination() + // Setting comparator to null lets statuses sort ascending + comparator = null + } + } + + override fun onLoadFinished(loader: Loader>, data: List?) { + val adapter = this@StatusFragment.adapter + adapter.updateItemDecoration() + val conversationLoader = loader as ConversationLoader + var supportedPositions: Long = 0 + if (data != null && !data.isEmpty()) { + val sinceSortId = (conversationLoader.pagination as? SinceMaxPagination)?.sinceSortId ?: -1 + if (sinceSortId < data[data.size - 1].sort_id) { + supportedPositions = supportedPositions or ILoadMoreSupportAdapter.END + } + if (data[0].in_reply_to_status_id != null) { + supportedPositions = supportedPositions or ILoadMoreSupportAdapter.START + } + } else { + supportedPositions = supportedPositions or ILoadMoreSupportAdapter.END + val status = status + if (status?.in_reply_to_status_id != null) { + supportedPositions = supportedPositions or ILoadMoreSupportAdapter.START + } + } + adapter.loadMoreSupportedPosition = supportedPositions + setConversation(data) + adapter.isConversationsLoading = false + adapter.isRepliesLoading = false + } + + override fun onLoaderReset(loader: Loader>) { + + } + } + + private val statusActivityLoaderCallback = object : LoaderCallbacks { + override fun onCreateLoader(id: Int, args: Bundle): Loader { + val accountKey = args.getParcelable(EXTRA_ACCOUNT_KEY) + val statusId = args.getString(EXTRA_STATUS_ID) + return StatusActivitySummaryLoader(activity, accountKey, statusId) + } + + override fun onLoadFinished(loader: Loader, data: StatusActivity?) { + adapter.updateItemDecoration() + adapter.statusActivity = data + } + + override fun onLoaderReset(loader: Loader) { + + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + val activity = activity ?: return + when (requestCode) { + REQUEST_SET_COLOR -> { + val status = adapter.status ?: return + if (resultCode == Activity.RESULT_OK) { + if (data == null) return + val color = data.getIntExtra(EXTRA_COLOR, Color.TRANSPARENT) + userColorNameManager.setUserColor(status.user_key, color) + } else if (resultCode == ColorPickerDialogActivity.RESULT_CLEARED) { + userColorNameManager.clearUserColor(status.user_key) + } + val args = arguments + if (args.containsKey(EXTRA_STATUS)) { + args.putParcelable(EXTRA_STATUS, status) + } + loaderManager.restartLoader(LOADER_ID_DETAIL_STATUS, args, this) + } + REQUEST_SELECT_ACCOUNT -> { + val status = adapter.status ?: return + if (resultCode == Activity.RESULT_OK) { + if (data == null || !data.hasExtra(EXTRA_ID)) return + val accountKey = data.getParcelableExtra(EXTRA_ACCOUNT_KEY) + IntentUtils.openStatus(activity, accountKey, status.id) + } + } + AbsStatusesFragment.REQUEST_FAVORITE_SELECT_ACCOUNT, + AbsStatusesFragment.REQUEST_RETWEET_SELECT_ACCOUNT -> { + AbsStatusesFragment.handleActionActivityResult(this, requestCode, resultCode, data) + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_status, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + setHasOptionsMenu(true) + Utils.setNdefPushMessageCallback(activity, CreateNdefMessageCallback { + val status = status ?: return@CreateNdefMessageCallback null + NdefMessage(arrayOf(NdefRecord.createUri(LinkCreator.getStatusWebLink(status)))) + }) + adapter = StatusDetailsAdapter(this) + layoutManager = StatusListLinearLayoutManager(context, recyclerView) + mItemDecoration = StatusDividerItemDecoration(context, adapter, layoutManager.orientation) + recyclerView.addItemDecoration(mItemDecoration) + layoutManager.recycleChildrenOnDetach = true + recyclerView.layoutManager = layoutManager + recyclerView.clipToPadding = false + adapter.statusClickListener = this + recyclerView.adapter = adapter + registerForContextMenu(recyclerView) + + scrollListener = RecyclerViewScrollHandler(this, RecyclerViewCallback(recyclerView)) + scrollListener.touchSlop = ViewConfiguration.get(context).scaledTouchSlop + + navigationHelper = RecyclerViewNavigationHelper(recyclerView, layoutManager, + adapter, null) + + setState(STATE_LOADING) + + loaderManager.initLoader(LOADER_ID_DETAIL_STATUS, arguments, this) + } + + override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) { + val status = adapter.getStatus(statusPosition) + IntentUtils.openMedia(activity, status, current, preferences[newDocumentApiKey], + preferences[displaySensitiveContentsKey]) + } + + override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) { + val status = adapter.getStatus(statusPosition) + val quotedMedia = status.quoted_media ?: return + IntentUtils.openMedia(activity, status.account_key, status.is_possibly_sensitive, status, + current, quotedMedia, preferences[newDocumentApiKey], + preferences[displaySensitiveContentsKey]) + } + + override fun onGapClick(holder: GapViewHolder, position: Int) { + + } + + override fun onItemActionClick(holder: ViewHolder, id: Int, position: Int) { + val status = adapter.getStatus(position) + handleActionClick(this@StatusFragment, id, status, holder as StatusViewHolder) + } + + + override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean { + val status = adapter.getStatus(position) + return AbsStatusesFragment.handleActionLongClick(this, status, adapter.getItemId(position), id) + } + + override fun onStatusClick(holder: IStatusViewHolder, position: Int) { + val status = adapter.getStatus(position) + IntentUtils.openStatus(activity, status) + } + + override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) { + val status = adapter.getStatus(position) + val quotedId = status.quoted_id ?: return + IntentUtils.openStatus(activity, status.account_key, quotedId) + } + + override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean { + return false + } + + override fun onItemMenuClick(holder: ViewHolder, menuView: View, position: Int) { + if (activity == null) return + val view = layoutManager.findViewByPosition(position) ?: return + recyclerView.showContextMenuForChild(view) + } + + override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) { + val status = adapter.getStatus(position) + IntentUtils.openUserProfile(activity, status.account_key, status.user_key, + status.user_screen_name, status.extras?.user_statusnet_profile_url, + preferences[newDocumentApiKey], null) + } + + override fun onMediaClick(view: View, current: ParcelableMedia, accountKey: UserKey?, id: Long) { + val status = adapter.status ?: return + if ((view.parent as View).id == R.id.quotedMediaPreview && status.quoted_media != null) { + IntentUtils.openMediaDirectly(activity, accountKey, status.quoted_media!!, current, + newDocument = preferences[newDocumentApiKey], status = status) + } else if (status.media != null) { + IntentUtils.openMediaDirectly(activity, accountKey, status.media!!, current, + newDocument = preferences[newDocumentApiKey], status = status) + } + } + + override fun handleKeyboardShortcutSingle(handler: KeyboardShortcutsHandler, + keyCode: Int, event: KeyEvent, + metaState: Int): Boolean { + if (!KeyboardShortcutsHandler.isValidForHotkey(keyCode, event)) return false + val focusedChild = RecyclerViewUtils.findRecyclerViewChild(recyclerView, layoutManager.focusedChild) + val position: Int + if (focusedChild != null && focusedChild.parent === recyclerView) { + position = recyclerView.getChildLayoutPosition(focusedChild) + } else { + return false + } + if (position == -1) return false + val status = adapter.getStatus(position) + val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: return false + return AbsStatusesFragment.handleKeyboardShortcutAction(this, action, status, position) + } + + override fun isKeyboardShortcutHandled(handler: KeyboardShortcutsHandler, keyCode: Int, event: KeyEvent, metaState: Int): Boolean { + val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: return false + when (action) { + ACTION_STATUS_REPLY, ACTION_STATUS_RETWEET, ACTION_STATUS_FAVORITE -> return true + } + return navigationHelper.isKeyboardShortcutHandled(handler, keyCode, event, metaState) + } + + override fun handleKeyboardShortcutRepeat(handler: KeyboardShortcutsHandler, + keyCode: Int, repeatCount: Int, + event: KeyEvent, metaState: Int): Boolean { + return navigationHelper.handleKeyboardShortcutRepeat(handler, keyCode, + repeatCount, event, metaState) + } + + override fun onCreateLoader(id: Int, args: Bundle): Loader> { + val fragmentArgs = arguments + val accountKey = fragmentArgs.getParcelable(EXTRA_ACCOUNT_KEY) + val statusId = fragmentArgs.getString(EXTRA_STATUS_ID) + return ParcelableStatusLoader(activity, false, fragmentArgs, accountKey, statusId) + } + + + override fun onLoadFinished(loader: Loader>, + data: SingleResponse) { + val activity = activity ?: return + val status = data.data + if (status != null) { + val readPosition = saveReadPosition() + val dataExtra = data.extras + val details: AccountDetails? = dataExtra.getParcelable(EXTRA_ACCOUNT) + if (adapter.setStatus(status, details)) { + val args = arguments + if (args.containsKey(EXTRA_STATUS)) { + args.putParcelable(EXTRA_STATUS, status) + } + adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.BOTH + adapter.setData(null) + loadConversation(status, null, null) + loadActivity(status) + + val position = adapter.getFirstPositionOfItem(StatusDetailsAdapter.ITEM_IDX_STATUS) + if (position != RecyclerView.NO_POSITION) { + layoutManager.scrollToPositionWithOffset(position, 0) + } + + Analyzer.log(StatusView(details?.type, status.media_type).apply { + this.type = StatusView.getStatusType(status) + this.source = status.source?.let(HtmlEscapeHelper::toPlainText) + }) + } else if (readPosition != null) { + restoreReadPosition(readPosition) + } + setState(STATE_LOADED) + } else { + adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.NONE + setState(STATE_ERROR) + val errorInfo = StatusCodeMessageUtils.getErrorInfo(context, data.exception!!) + errorText.spannable = errorInfo.message + errorIcon.setImageResource(errorInfo.icon) + } + activity.invalidateOptionsMenu() + } + + override fun onLoaderReset(loader: Loader>) { + } + + override val refreshing: Boolean + get() = loaderManager.hasRunningLoadersSafe() + + override fun onLoadMoreContents(@IndicatorPosition position: Long) { + if (!hasMoreConversation) return + if (ILoadMoreSupportAdapter.START in position) { + val start = adapter.getIndexStart(StatusDetailsAdapter.ITEM_IDX_CONVERSATION) + val first = adapter.getStatus(start, true) + if (first.in_reply_to_status_id == null) return + loadConversation(status, null, first.id) + } else if (ILoadMoreSupportAdapter.END in position) { + val start = adapter.getIndexStart(StatusDetailsAdapter.ITEM_IDX_CONVERSATION) + val last = adapter.getStatus(start + adapter.getStatusCount(true) - 1, true) + loadConversation(status, last.id, null) + } + adapter.loadMoreIndicatorPosition = position + } + + override fun setControlVisible(visible: Boolean) { + // No-op + } + + override fun onApplySystemWindowInsets(insets: Rect) { + recyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom) + } + + override val reachingEnd: Boolean + get() { + val lm = layoutManager + var itemPos = lm.findLastCompletelyVisibleItemPosition() + if (itemPos == RecyclerView.NO_POSITION) { + // No completely visible item, find visible item instead + itemPos = lm.findLastVisibleItemPosition() + } + return itemPos >= lm.itemCount - 1 + } + + override val reachingStart: Boolean + get() { + val lm = layoutManager + var itemPos = lm.findFirstCompletelyVisibleItemPosition() + if (itemPos == RecyclerView.NO_POSITION) { + // No completely visible item, find visible item instead + itemPos = lm.findFirstVisibleItemPosition() + } + return itemPos <= 1 + } + + private val status: ParcelableStatus? + get() = adapter.status + + private fun loadConversation(status: ParcelableStatus?, sinceId: String?, maxId: String?) { + if (status == null || activity == null) return + val args = Bundle { + this[EXTRA_ACCOUNT_KEY] = status.account_key + this[EXTRA_STATUS_ID] = status.originalId + this[EXTRA_SINCE_ID] = sinceId + this[EXTRA_MAX_ID] = maxId + this[EXTRA_STATUS] = status + } + if (conversationLoaderInitialized) { + loaderManager.restartLoader(LOADER_ID_STATUS_CONVERSATIONS, args, conversationsLoaderCallback) + return + } + loaderManager.initLoader(LOADER_ID_STATUS_CONVERSATIONS, args, conversationsLoaderCallback) + conversationLoaderInitialized = true + } + + + private fun loadActivity(status: ParcelableStatus?) { + if (status == null || host == null || isDetached) return + val args = Bundle { + this[EXTRA_ACCOUNT_KEY] = status.account_key + this[EXTRA_STATUS_ID] = status.originalId + } + if (activityLoaderInitialized) { + loaderManager.restartLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback) + return + } + loaderManager.initLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback) + activityLoaderInitialized = true + } + + internal fun loadTranslation(status: ParcelableStatus?) { + if (status == null) return + if (loadTranslationTask?.isFinished == true) return + loadTranslationTask = run { + val task = LoadTranslationTask(this, status) + TaskStarter.execute(task) + return@run task + } + } + + private fun setConversation(data: List?) { + val readPosition = saveReadPosition() + val changed = adapter.setData(data) + hasMoreConversation = data != null && changed + restoreReadPosition(readPosition) + } + + private fun scrollToCurrent() { + if (adapter.status != null) { + val position = adapter.getFirstPositionOfItem(StatusDetailsAdapter.ITEM_IDX_STATUS) + recyclerView.smoothScrollToPosition(position) + } + } + + private fun displayTranslation(translation: TranslationResult) { + adapter.translationResult = translation + } + + private fun saveReadPosition(): ReadPosition? { + val lm = layoutManager + val adapter = this.adapter + val position = lm.findFirstVisibleItemPosition() + if (position == RecyclerView.NO_POSITION) return null + val itemType = adapter.getItemType(position) + var itemId = adapter.getItemId(position) + val positionView: View? + if (itemType == StatusDetailsAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) { + // Should be next item + positionView = lm.findViewByPosition(position + 1) + itemId = adapter.getItemId(position + 1) + } else { + positionView = lm.findViewByPosition(position) + } + return ReadPosition(itemId, positionView?.top ?: 0) + } + + private fun restoreReadPosition(position: ReadPosition?) { + val adapter = this.adapter + if (position == null) return + val adapterPosition = adapter.findPositionByItemId(position.statusId) + if (adapterPosition < 0) return + layoutManager.scrollToPositionWithOffset(adapterPosition, position.offsetTop) + } + + private fun setState(state: Int) { + statusContent.visibility = if (state == STATE_LOADED) View.VISIBLE else View.GONE + progressContainer.visibility = if (state == STATE_LOADING) View.VISIBLE else View.GONE + errorContainer.visibility = if (state == STATE_ERROR) View.VISIBLE else View.GONE + } + + override fun onStart() { + super.onStart() + bus.register(this) + recyclerView.addOnScrollListener(scrollListener) + recyclerView.setOnTouchListener(scrollListener.touchListener) + } + + override fun onStop() { + recyclerView.setOnTouchListener(null) + recyclerView.removeOnScrollListener(scrollListener) + bus.unregister(this) + super.onStop() + } + + override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { + if (!userVisibleHint) return + val contextMenuInfo = menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return + val status = adapter.getStatus(contextMenuInfo.position) + val inflater = MenuInflater(context) + inflater.inflate(R.menu.action_status, menu) + MenuUtils.setupForStatus(context, menu, preferences, twitterWrapper, userColorNameManager, + status) + } + + override fun onContextItemSelected(item: MenuItem): Boolean { + if (!userVisibleHint) return false + val contextMenuInfo = item.menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return false + val status = adapter.getStatus(contextMenuInfo.position) + if (item.itemId == R.id.share) { + val shareIntent = Utils.createStatusShareIntent(activity, status) + val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status)) + + startActivity(chooser) + + val am = AccountManager.get(context) + val accountType = AccountUtils.findByAccountKey(am, status.account_key)?.getAccountType(am) + + Analyzer.log(Share.status(accountType, status)) + return true + } + return MenuUtils.handleStatusClick(activity, this, fragmentManager, + preferences, userColorNameManager, twitterWrapper, status, item) + } + + @Subscribe + fun notifyStatusListChanged(event: StatusListChangedEvent) { + adapter.notifyDataSetChanged() + } + + @Subscribe + fun notifyFavoriteTask(event: FavoriteTaskEvent) { + if (!event.isSucceeded) return + val status = adapter.findStatusById(event.accountKey, event.statusId) + if (status != null) { + when (event.action) { + FavoriteTaskEvent.Action.CREATE -> { + status.is_favorite = true + } + FavoriteTaskEvent.Action.DESTROY -> { + status.is_favorite = false + } + } + } + } + + internal fun onUserClick(user: ParcelableUser) { + IntentUtils.openUserProfile(context, user, true, null) + } + + internal fun openTranslationDestinationChooser() { + val account = adapter.statusAccount ?: return + val weakThis = WeakReference(this) + (showProgressDialog("get_language_settings") and task { + val fragment = weakThis.get() ?: throw InterruptedException() + val microBlog = account.newMicroBlogInstance(fragment.context, MicroBlog::class.java) + return@task Pair(microBlog.accountSettings.language, + microBlog.languages.map { TranslationDestinationDialogFragment.DisplayLanguage(it.name, it.code) }) + }).successUi { (_, settings) -> + val (accountLanguage, languages) = settings + val fragment = weakThis.get() ?: return@successUi + val df = TranslationDestinationDialogFragment.create(languages, accountLanguage) + df.show(fragment.childFragmentManager, "translation_destination_settings") + }.alwaysUi { + val fragment = weakThis.get() ?: return@alwaysUi + fragment.dismissProgressDialog("get_language_settings") + } + } + + class LoadSensitiveImageConfirmDialogFragment : BaseDialogFragment(), DialogInterface.OnClickListener { + + override fun onClick(dialog: DialogInterface, which: Int) { + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + val f = parentFragment + if (f is StatusFragment) { + val adapter = f.adapter + adapter.isDetailMediaExpanded = true + } + } + } + + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = activity + val builder = AlertDialog.Builder(context) + builder.setTitle(android.R.string.dialog_alert_title) + builder.setMessage(R.string.sensitive_content_warning) + builder.setPositiveButton(android.R.string.ok, this) + builder.setNegativeButton(android.R.string.cancel, null) + val dialog = builder.create() + dialog.onShow { it.applyTheme() } + return dialog + } + } + + internal class LoadTranslationTask(fragment: StatusFragment, val status: ParcelableStatus) : + AbsAccountRequestTask(fragment.context, status.account_key) { + + private val weakFragment = WeakReference(fragment) + + override fun onExecute(account: AccountDetails, params: Any?): TranslationResult { + val twitter = account.newMicroBlogInstance(context, MicroBlog::class.java) + val prefDest = preferences.getString(KEY_TRANSLATION_DESTINATION, null) + val dest: String + if (TextUtils.isEmpty(prefDest)) { + dest = twitter.accountSettings.language + val editor = preferences.edit() + editor.putString(KEY_TRANSLATION_DESTINATION, dest) + editor.apply() + } else { + dest = prefDest + } + return twitter.showTranslation(status.originalId, dest) + } + + override fun onSucceed(callback: Any?, result: TranslationResult) { + val fragment = weakFragment.get() ?: return + fragment.displayTranslation(result) + } + + override fun onException(callback: Any?, exception: MicroBlogException) { + Toast.makeText(context, exception.getErrorMessage(context), Toast.LENGTH_SHORT).show() + } + } + + + class StatusActivitySummaryLoader( + context: Context, + private val accountKey: UserKey, + private val statusId: String + ) : FixedAsyncTaskLoader(context) { + + override fun loadInBackground(): StatusActivity? { + val context = context + val details = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true) ?: return null + if (AccountType.TWITTER != details.type) { + return null + } + val twitter = MicroBlogAPIFactory.getInstance(context, accountKey) ?: return null + val paging = Paging() + paging.setCount(10) + val activitySummary = StatusActivity(statusId, emptyList()) + try { + activitySummary.retweeters = twitter.getRetweets(statusId, paging) + .filterNot { DataStoreUtils.isFilteringUser(context, it.user.key) } + .distinctBy { it.user.id } + .map { it.user.toParcelable(details) } + val countValues = ContentValues() + val status = twitter.showStatus(statusId) + activitySummary.favoriteCount = status.favoriteCount + activitySummary.retweetCount = status.retweetCount + activitySummary.replyCount = status.replyCount + + countValues.put(Statuses.REPLY_COUNT, activitySummary.replyCount) + countValues.put(Statuses.FAVORITE_COUNT, activitySummary.favoriteCount) + countValues.put(Statuses.RETWEET_COUNT, activitySummary.retweetCount) + + val cr = context.contentResolver + val statusWhere = Expression.and( + Expression.equalsArgs(Statuses.ACCOUNT_KEY), + Expression.or( + Expression.equalsArgs(Statuses.ID), + Expression.equalsArgs(Statuses.RETWEET_ID))) + val statusWhereArgs = arrayOf(accountKey.toString(), statusId, statusId) + cr.update(Statuses.CONTENT_URI, countValues, statusWhere.sql, statusWhereArgs) + cr.updateStatusInfo(DataStoreUtils.STATUSES_ACTIVITIES_URIS, Statuses.COLUMNS, + accountKey, statusId, ParcelableStatus::class.java) { item -> + item.favorite_count = activitySummary.favoriteCount + item.reply_count = activitySummary.replyCount + item.retweet_count = activitySummary.retweetCount + return@updateStatusInfo item + } + val pStatus = status.toParcelable(details) + cr.insert(CachedStatuses.CONTENT_URI, ObjectCursor + .valuesCreatorFrom(ParcelableStatus::class.java).create(pStatus)) + + return activitySummary + } catch (e: MicroBlogException) { + return null + } + + } + + override fun onStartLoading() { + forceLoad() + } + } + + data class StatusActivity( + var statusId: String, + var retweeters: List, + var favoriteCount: Long = 0, + var replyCount: Long = -1, + var retweetCount: Long = 0 + ) { + + fun isStatus(status: ParcelableStatus): Boolean { + return statusId == status.retweet_id ?: status.id + } + } + + data class ReadPosition(var statusId: Long, var offsetTop: Int) + + private class StatusListLinearLayoutManager(context: Context, private val recyclerView: RecyclerView) : FixedLinearLayoutManager(context) { + private var spaceHeight: Int = 0 + + init { + orientation = LinearLayoutManager.VERTICAL + } + + override fun getDecoratedMeasuredHeight(child: View): Int { + if (getItemViewType(child) == StatusDetailsAdapter.VIEW_TYPE_SPACE) { + val height = calculateSpaceItemHeight(child, StatusDetailsAdapter.VIEW_TYPE_SPACE, + StatusDetailsAdapter.VIEW_TYPE_DETAIL_STATUS) + if (height >= 0) { + return height + } + } + return super.getDecoratedMeasuredHeight(child) + } + + override fun setOrientation(orientation: Int) { + if (orientation != LinearLayoutManager.VERTICAL) + throw IllegalArgumentException("Only VERTICAL orientation supported") + super.setOrientation(orientation) + } + + + override fun computeVerticalScrollExtent(state: RecyclerView.State?): Int { + val firstPosition = findFirstVisibleItemPosition() + val lastPosition = Math.min(validScrollItemCount - 1, findLastVisibleItemPosition()) + if (firstPosition < 0 || lastPosition < 0) return 0 + val childCount = lastPosition - firstPosition + 1 + if (childCount > 0) { + if (isSmoothScrollbarEnabled) { + var extent = childCount * 100 + var view = findViewByPosition(firstPosition) ?: return 0 + val top = view.top + var height = view.height + if (height > 0) { + extent += top * 100 / height + } + + view = findViewByPosition(lastPosition) ?: return 0 + val bottom = view.bottom + height = view.height + if (height > 0) { + extent -= (bottom - getHeight()) * 100 / height + } + return extent + } else { + return 1 + } + } + return 0 + } + + override fun computeVerticalScrollOffset(state: RecyclerView.State?): Int { + val firstPosition = findFirstVisibleItemPosition() + val lastPosition = Math.min(validScrollItemCount - 1, findLastVisibleItemPosition()) + if (firstPosition < 0 || lastPosition < 0) return 0 + val childCount = lastPosition - firstPosition + 1 + val skippedCount = skippedScrollItemCount + if (firstPosition >= skippedCount && childCount > 0) { + if (isSmoothScrollbarEnabled) { + val view = findViewByPosition(firstPosition) ?: return 0 + val top = view.top + val height = view.height + if (height > 0) { + return Math.max((firstPosition - skippedCount) * 100 - top * 100 / height, 0) + } + } else { + val index: Int + val count = validScrollItemCount + if (firstPosition == 0) { + index = 0 + } else if (firstPosition + childCount == count) { + index = count + } else { + index = firstPosition + childCount / 2 + } + return (firstPosition + childCount * (index / count.toFloat())).toInt() + } + } + return 0 + } + + override fun computeVerticalScrollRange(state: RecyclerView.State?): Int { + val result: Int + if (isSmoothScrollbarEnabled) { + result = Math.max(validScrollItemCount * 100, 0) + } else { + result = validScrollItemCount + } + return result + } + + private val skippedScrollItemCount: Int + get() { + val adapter = recyclerView.adapter as StatusDetailsAdapter + var skipped = 0 + if (!adapter.isConversationsLoading) { + skipped += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) + } + return skipped + } + + private val validScrollItemCount: Int + get() { + val adapter = recyclerView.adapter as StatusDetailsAdapter + var count = 0 + if (adapter.isConversationsLoading) { + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) + } + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION_ERROR) + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION) + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_STATUS) + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_REPLY) + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_REPLY_ERROR) + if (adapter.isRepliesLoading) { + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_REPLY_LOAD_MORE) + } + val spaceHeight = calculateSpaceHeight() + if (spaceHeight > 0) { + count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_SPACE) + } + return count + } + + private fun calculateSpaceHeight(): Int { + val space = findViewByPosition(itemCount - 1) ?: return spaceHeight + spaceHeight = getDecoratedMeasuredHeight(space) + return spaceHeight + } + + + } + + private class StatusDividerItemDecoration( + context: Context, + private val statusAdapter: StatusDetailsAdapter, + orientation: Int + ) : ExtendedDividerItemDecoration(context, orientation) { + + override fun isDividerEnabled(childPos: Int): Boolean { + if (childPos >= statusAdapter.itemCount || childPos < 0) return false + val itemType = statusAdapter.getItemType(childPos) + when (itemType) { + StatusDetailsAdapter.ITEM_IDX_REPLY_LOAD_MORE, StatusDetailsAdapter.ITEM_IDX_REPLY_ERROR, + StatusDetailsAdapter.ITEM_IDX_SPACE -> return false + } + return true + } + + } + + companion object { + + // Constants + private val LOADER_ID_DETAIL_STATUS = 1 + private val LOADER_ID_STATUS_CONVERSATIONS = 2 + private val LOADER_ID_STATUS_ACTIVITY = 3 + private val STATE_LOADED = 1 + private val STATE_LOADING = 2 + private val STATE_ERROR = 3 + + fun Bundle.toPagination(): Pagination { + val maxId = getString(EXTRA_MAX_ID) + val sinceId = getString(EXTRA_SINCE_ID) + val maxSortId = getLong(EXTRA_MAX_SORT_ID) + val sinceSortId = getLong(EXTRA_SINCE_SORT_ID) + return SinceMaxPagination().apply { + this.maxId = maxId + this.sinceId = sinceId + this.maxSortId = maxSortId + this.sinceSortId = sinceSortId + } + } + + } +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/TranslationDestinationDialogFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/TranslationDestinationDialogFragment.kt new file mode 100644 index 000000000..18002dea9 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/TranslationDestinationDialogFragment.kt @@ -0,0 +1,114 @@ +/* + * 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.fragment.status + +import android.app.Dialog +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.support.v7.app.AlertDialog +import org.mariotaku.kpreferences.get +import org.mariotaku.kpreferences.set +import org.mariotaku.ktextension.Bundle +import org.mariotaku.ktextension.getTypedArray +import org.mariotaku.ktextension.mapToArray +import org.mariotaku.ktextension.set +import org.mariotaku.twidere.constant.translationDestinationKey +import org.mariotaku.twidere.extension.applyTheme +import org.mariotaku.twidere.extension.onShow +import org.mariotaku.twidere.fragment.BaseDialogFragment +import java.text.Collator +import java.util.* + +class TranslationDestinationDialogFragment : BaseDialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = AlertDialog.Builder(context) + val languages = arguments.getTypedArray(EXTRA_LANGUAGES).sortedArrayWith(LanguageComparator()) + val selectedLanguage = preferences[translationDestinationKey] ?: arguments.getString(EXTRA_SELECTED_LANGUAGE) + val selectedIndex = languages.indexOfFirst { selectedLanguage == it.code } + builder.setSingleChoiceItems(languages.mapToArray { it.name }, selectedIndex) { _, which -> + preferences[translationDestinationKey] = languages[which].code + } + builder.setPositiveButton(android.R.string.ok) { _, _ -> + + } + builder.setNegativeButton(android.R.string.cancel, null) + val dialog = builder.create() + dialog.onShow { + it.applyTheme() + it.listView?.isFastScrollEnabled = true + } + return dialog + } + + data class DisplayLanguage(val name: String, val code: String) : Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString()) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(name) + parcel.writeString(code) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + + override fun createFromParcel(parcel: Parcel): DisplayLanguage { + return DisplayLanguage(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + } + } + + + private class LanguageComparator : Comparator { + + private val collator = Collator.getInstance(Locale.getDefault()) + + override fun compare(object1: DisplayLanguage, object2: DisplayLanguage): Int { + return collator.compare(object1.name, object2.name) + } + + } + + companion object { + const val EXTRA_SELECTED_LANGUAGE = "selected_language" + const val EXTRA_LANGUAGES = "languages" + + fun create(languages: List, selectedLanguage: String?): TranslationDestinationDialogFragment { + val df = TranslationDestinationDialogFragment() + df.arguments = Bundle { + this[EXTRA_LANGUAGES] = languages.toTypedArray() + this[EXTRA_SELECTED_LANGUAGE] = selectedLanguage + } + return df + } + } +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt new file mode 100644 index 000000000..a08ade0b2 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt @@ -0,0 +1,760 @@ +/* + * 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.status + +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Rect +import android.support.annotation.UiThread +import android.support.v4.content.ContextCompat +import android.support.v4.view.MenuItemCompat +import android.support.v4.view.ViewCompat +import android.support.v7.widget.ActionMenuView +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextUtils +import android.text.method.LinkMovementMethod +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import com.bumptech.glide.Glide +import kotlinx.android.synthetic.main.adapter_item_status_count_label.view.* +import kotlinx.android.synthetic.main.header_status.view.* +import org.mariotaku.kpreferences.get +import org.mariotaku.ktextension.applyFontFamily +import org.mariotaku.ktextension.hideIfEmpty +import org.mariotaku.ktextension.spannable +import org.mariotaku.microblog.library.twitter.model.TranslationResult +import org.mariotaku.twidere.Constants +import org.mariotaku.twidere.R +import org.mariotaku.twidere.adapter.BaseRecyclerViewAdapter +import org.mariotaku.twidere.adapter.StatusDetailsAdapter +import org.mariotaku.twidere.annotation.ProfileImageSize +import org.mariotaku.twidere.constant.displaySensitiveContentsKey +import org.mariotaku.twidere.constant.newDocumentApiKey +import org.mariotaku.twidere.extension.loadProfileImage +import org.mariotaku.twidere.extension.model.* +import org.mariotaku.twidere.fragment.status.StatusFragment +import org.mariotaku.twidere.menu.FavoriteItemProvider +import org.mariotaku.twidere.model.* +import org.mariotaku.twidere.model.util.ParcelableLocationUtils +import org.mariotaku.twidere.model.util.ParcelableMediaUtils +import org.mariotaku.twidere.util.* +import org.mariotaku.twidere.util.twitter.card.TwitterCardViewFactory +import org.mariotaku.twidere.view.ProfileImageView +import java.util.* + +class DetailStatusViewHolder( + private val adapter: StatusDetailsAdapter, + itemView: View +) : RecyclerView.ViewHolder(itemView), View.OnClickListener, ActionMenuView.OnMenuItemClickListener { + + private val linkClickHandler: StatusLinkClickHandler + private val linkify: TwidereLinkify + + private val profileTypeView = itemView.profileType + private val nameView = itemView.name + private val summaryView = itemView.summary + private val textView = itemView.text + private val locationView = itemView.locationView + private val retweetedByView = itemView.retweetedBy + private val translateResultView = itemView.translateResult + private val translateChangeLanguageView = itemView.translateChangeLanguage + private val translateContainer = itemView.translateContainer + private val translateLabelView = itemView.translateLabel + + + init { + this.linkClickHandler = DetailStatusLinkClickHandler(adapter.context, + adapter.multiSelectManager, adapter, adapter.preferences) + this.linkify = TwidereLinkify(linkClickHandler) + + initViews() + } + + @UiThread + fun displayStatus(account: AccountDetails?, status: ParcelableStatus?, + statusActivity: StatusFragment.StatusActivity?, translation: TranslationResult?) { + if (account == null || status == null) return + val fragment = adapter.fragment + val context = adapter.context + val formatter = adapter.bidiFormatter + val twitter = adapter.twitterWrapper + val nameFirst = adapter.nameFirst + val colorNameManager = adapter.userColorNameManager + + linkClickHandler.status = status + + if (status.retweet_id != null) { + val retweetedBy = colorNameManager.getDisplayName(status.retweeted_by_user_key!!, + status.retweeted_by_user_name!!, status.retweeted_by_user_acct!!, nameFirst) + retweetedByView.spannable = context.getString(R.string.name_retweeted, retweetedBy) + retweetedByView.visibility = View.VISIBLE + } else { + retweetedByView.spannable = null + retweetedByView.visibility = View.GONE + } + + itemView.profileContainer.drawEnd(status.account_color) + + val layoutPosition = layoutPosition + val skipLinksInText = status.extras?.support_entities == true + + if (status.is_quote) { + + itemView.quotedView.visibility = View.VISIBLE + + val quoteContentAvailable = status.quoted_text_plain != null && status.quoted_text_unescaped != null + + if (quoteContentAvailable) { + itemView.quotedName.visibility = View.VISIBLE + itemView.quotedText.visibility = View.VISIBLE + + itemView.quotedName.name = colorNameManager.getUserNickname(status.quoted_user_key!!, + status.quoted_user_name) + itemView.quotedName.screenName = "@${status.quoted_user_acct}" + itemView.quotedName.updateText(formatter) + + + val quotedDisplayEnd = status.extras?.quoted_display_text_range?.getOrNull(1) ?: -1 + val quotedText = SpannableStringBuilder.valueOf(status.quoted_text_unescaped) + status.quoted_spans?.applyTo(quotedText) + linkify.applyAllLinks(quotedText, status.account_key, layoutPosition.toLong(), + status.is_possibly_sensitive, skipLinksInText) + if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) { + itemView.quotedText.spannable = quotedText.subSequence(0, quotedDisplayEnd) + } else { + itemView.quotedText.spannable = quotedText + } + itemView.quotedText.hideIfEmpty() + + val quotedUserColor = colorNameManager.getUserColor(status.quoted_user_key!!) + if (quotedUserColor != 0) { + itemView.quotedView.drawStart(quotedUserColor) + } else { + itemView.quotedView.drawStart(ThemeUtils.getColorFromAttribute(context, + R.attr.quoteIndicatorBackgroundColor)) + } + + val quotedMedia = status.quoted_media + + if (quotedMedia?.isEmpty() != false) { + itemView.quotedMediaLabel.visibility = View.GONE + itemView.quotedMediaPreview.visibility = View.GONE + } else if (adapter.isDetailMediaExpanded) { + itemView.quotedMediaLabel.visibility = View.GONE + itemView.quotedMediaPreview.visibility = View.VISIBLE + itemView.quotedMediaPreview.displayMedia(adapter.requestManager, + media = quotedMedia, accountKey = status.account_key, + mediaClickListener = adapter.fragment) + } else { + itemView.quotedMediaLabel.visibility = View.VISIBLE + itemView.quotedMediaPreview.visibility = View.GONE + } + } else { + itemView.quotedName.visibility = View.GONE + itemView.quotedText.visibility = View.VISIBLE + itemView.quotedMediaLabel.visibility = View.GONE + itemView.quotedMediaPreview.visibility = View.GONE + + // Not available + val string = SpannableString.valueOf(context.getString(R.string.label_status_not_available)) + string.setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context, + android.R.attr.textColorTertiary, textView.currentTextColor)), 0, + string.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + itemView.quotedText.spannable = string + + itemView.quotedView.drawStart(ThemeUtils.getColorFromAttribute(context, + R.attr.quoteIndicatorBackgroundColor)) + } + } else { + itemView.quotedView.visibility = View.GONE + } + + itemView.profileContainer.drawStart(colorNameManager.getUserColor(status.user_key)) + + val timestamp: Long + + if (status.is_retweet) { + timestamp = status.retweet_timestamp + } else { + timestamp = status.timestamp + } + + nameView.name = colorNameManager.getUserNickname(status.user_key, status.user_name) + nameView.screenName = "@${status.user_acct}" + nameView.updateText(formatter) + + adapter.requestManager.loadProfileImage(context, status, adapter.profileImageStyle, + itemView.profileImage.cornerRadius, itemView.profileImage.cornerRadiusRatio, + size = ProfileImageSize.ORIGINAL).into(itemView.profileImage) + + val typeIconRes = Utils.getUserTypeIconRes(status.user_is_verified, status.user_is_protected) + val typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.user_is_verified, status.user_is_protected) + + + if (typeIconRes != 0 && typeDescriptionRes != 0) { + profileTypeView.setImageResource(typeIconRes) + profileTypeView.contentDescription = context.getString(typeDescriptionRes) + profileTypeView.visibility = View.VISIBLE + } else { + profileTypeView.setImageDrawable(null) + profileTypeView.contentDescription = null + profileTypeView.visibility = View.GONE + } + + val timeString = Utils.formatToLongTimeString(context, timestamp)?.takeIf(String::isNotEmpty) + val source = status.source?.takeIf(String::isNotEmpty) + itemView.timeSource.spannable = when { + timeString != null && source != null -> { + HtmlSpanBuilder.fromHtml(context.getString(R.string.status_format_time_source, + timeString, source)) + } + source != null -> HtmlSpanBuilder.fromHtml(source) + timeString != null -> timeString + else -> null + } + itemView.timeSource.movementMethod = LinkMovementMethod.getInstance() + + val displayEnd = status.extras?.display_text_range?.getOrNull(1) ?: -1 + val text = SpannableStringBuilder.valueOf(status.text_unescaped).apply { + status.spans?.applyTo(this) + linkify.applyAllLinks(this, status.account_key, layoutPosition.toLong(), + status.is_possibly_sensitive, skipLinksInText) + } + + summaryView.spannable = status.extras?.summary_text + summaryView.hideIfEmpty() + + if (displayEnd != -1 && displayEnd <= text.length) { + textView.spannable = text.subSequence(0, displayEnd) + } else { + textView.spannable = text + } + textView.hideIfEmpty() + + val location: ParcelableLocation? = status.location + val placeFullName: String? = status.place_full_name + + if (!TextUtils.isEmpty(placeFullName)) { + locationView.visibility = View.VISIBLE + locationView.spannable = placeFullName + locationView.isClickable = ParcelableLocationUtils.isValidLocation(location) + } else if (ParcelableLocationUtils.isValidLocation(location)) { + locationView.visibility = View.VISIBLE + locationView.setText(R.string.action_view_map) + locationView.isClickable = true + } else { + locationView.visibility = View.GONE + locationView.spannable = null + } + + val interactUsersAdapter = itemView.countsUsers.adapter as CountsUsersAdapter + if (statusActivity != null) { + updateStatusActivity(statusActivity) + } else { + interactUsersAdapter.setUsers(null) + interactUsersAdapter.setCounts(status) + } + + if (interactUsersAdapter.itemCount > 0) { + itemView.countsUsers.visibility = View.VISIBLE + itemView.countsUsersHeightHolder.visibility = View.INVISIBLE + } else { + itemView.countsUsers.visibility = View.GONE + itemView.countsUsersHeightHolder.visibility = View.GONE + } + + val media = status.media + + if (media?.isEmpty() != false) { + itemView.mediaPreviewContainer.visibility = View.GONE + itemView.mediaPreview.visibility = View.GONE + itemView.mediaPreviewLoad.visibility = View.GONE + itemView.mediaPreview.displayMedia() + } else if (adapter.isDetailMediaExpanded) { + itemView.mediaPreviewContainer.visibility = View.VISIBLE + itemView.mediaPreview.visibility = View.VISIBLE + itemView.mediaPreviewLoad.visibility = View.GONE + itemView.mediaPreview.displayMedia(adapter.requestManager, media = media, + accountKey = status.account_key, mediaClickListener = adapter.fragment) + } else { + itemView.mediaPreviewContainer.visibility = View.VISIBLE + itemView.mediaPreview.visibility = View.GONE + itemView.mediaPreviewLoad.visibility = View.VISIBLE + itemView.mediaPreview.displayMedia() + } + + if (TwitterCardUtils.isCardSupported(status)) { + val size = TwitterCardUtils.getCardSize(status.card!!) + + if (size != null) { + itemView.twitterCard.setCardSize(size.x, size.y) + } else { + itemView.twitterCard.setCardSize(0, 0) + } + val vc = TwitterCardViewFactory.from(status) + itemView.twitterCard.viewController = vc + if (vc != null) { + itemView.twitterCard.visibility = View.VISIBLE + } else { + itemView.twitterCard.visibility = View.GONE + } + + } else { + itemView.twitterCard.viewController = null + itemView.twitterCard.visibility = View.GONE + } + + MenuUtils.setupForStatus(context, itemView.menuBar.menu, fragment.preferences, twitter, + colorNameManager, status, adapter.statusAccount!!) + + + val lang = status.lang + if (CheckUtils.isValidLocale(lang) && account.isOfficial(context)) { + val locale = Locale(lang) + translateContainer.visibility = View.VISIBLE + if (translation != null) { + translateLabelView.text = context.getString(R.string.label_translation) + translateResultView.visibility = View.VISIBLE + translateChangeLanguageView.visibility = View.VISIBLE + translateResultView.text = translation.text + } else { + translateLabelView.text = context.getString(R.string.label_translate_from_language, + locale.displayLanguage) + translateResultView.visibility = View.GONE + translateChangeLanguageView.visibility = View.GONE + } + } else { + translateLabelView.setText(R.string.unknown_language) + translateContainer.visibility = View.GONE + } + + textView.setTextIsSelectable(true) + translateResultView.setTextIsSelectable(true) + + textView.movementMethod = LinkMovementMethod.getInstance() + itemView.quotedText.movementMethod = null + } + + override fun onClick(v: View) { + val status = adapter.getStatus(layoutPosition) + val fragment = adapter.fragment + val preferences = fragment.preferences + when (v) { + itemView.mediaPreviewLoad -> { + if (adapter.sensitiveContentEnabled || !status.is_possibly_sensitive) { + adapter.isDetailMediaExpanded = true + } else { + val f = StatusFragment.LoadSensitiveImageConfirmDialogFragment() + f.show(fragment.childFragmentManager, "load_sensitive_image_confirm") + } + } + itemView.profileContainer -> { + val activity = fragment.activity + IntentUtils.openUserProfile(activity, status.account_key, status.user_key, + status.user_screen_name, status.extras?.user_statusnet_profile_url, + preferences[newDocumentApiKey], null) + } + retweetedByView -> { + if (status.retweet_id != null) { + IntentUtils.openUserProfile(adapter.context, status.account_key, + status.retweeted_by_user_key, status.retweeted_by_user_screen_name, + null, preferences[newDocumentApiKey], null) + } + } + locationView -> { + val location = status.location + if (!ParcelableLocationUtils.isValidLocation(location)) return + IntentUtils.openMap(adapter.context, location.latitude, location.longitude) + } + itemView.quotedView -> { + val quotedId = status.quoted_id ?: return + IntentUtils.openStatus(adapter.context, status.account_key, quotedId) + } + translateLabelView -> { + fragment.loadTranslation(adapter.status) + } + translateChangeLanguageView -> { + fragment.openTranslationDestinationChooser() + } + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + val layoutPosition = layoutPosition + if (layoutPosition < 0) return false + val fragment = adapter.fragment + val status = adapter.getStatus(layoutPosition) + val preferences = fragment.preferences + val twitter = fragment.twitterWrapper + val manager = fragment.userColorNameManager + val activity = fragment.activity + return MenuUtils.handleStatusClick(activity, fragment, fragment.childFragmentManager, + preferences, manager, twitter, status, item) + } + + internal fun updateStatusActivity(activity: StatusFragment.StatusActivity) { + val adapter = itemView.countsUsers.adapter as CountsUsersAdapter + adapter.setUsers(activity.retweeters) + adapter.setCounts(activity) + } + + private fun initViews() { + itemView.menuBar.setOnMenuItemClickListener(this) + val fragment = adapter.fragment + val activity = fragment.activity + val inflater = activity.menuInflater + val menu = itemView.menuBar.menu + inflater.inflate(R.menu.menu_detail_status, menu) + val favoriteItem = menu.findItem(R.id.favorite) + val provider = MenuItemCompat.getActionProvider(favoriteItem) + if (provider is FavoriteItemProvider) { + val defaultColor = ThemeUtils.getActionIconColor(activity) + provider.setDefaultColor(defaultColor) + val favoriteHighlight = ContextCompat.getColor(activity, R.color.highlight_favorite) + val likeHighlight = ContextCompat.getColor(activity, R.color.highlight_like) + val useStar = adapter.useStarsForLikes + provider.setActivatedColor(if (useStar) favoriteHighlight else likeHighlight) + provider.setIcon(if (useStar) R.drawable.ic_action_star else R.drawable.ic_action_heart) + provider.setUseStar(useStar) + provider.init(itemView.menuBar, favoriteItem) + } + ThemeUtils.wrapMenuIcon(itemView.menuBar, excludeGroups = Constants.MENU_GROUP_STATUS_SHARE) + itemView.mediaPreviewLoad.setOnClickListener(this) + itemView.profileContainer.setOnClickListener(this) + retweetedByView.setOnClickListener(this) + locationView.setOnClickListener(this) + itemView.quotedView.setOnClickListener(this) + translateLabelView.setOnClickListener(this) + translateChangeLanguageView.setOnClickListener(this) + + val textSize = adapter.textSize + + nameView.setPrimaryTextSize(textSize * 1.25f) + nameView.setSecondaryTextSize(textSize * 0.85f) + summaryView.textSize = textSize * 1.25f + textView.textSize = textSize * 1.25f + + itemView.quotedName.setPrimaryTextSize(textSize * 1.25f) + itemView.quotedName.setSecondaryTextSize(textSize * 0.85f) + itemView.quotedText.textSize = textSize * 1.25f + + locationView.textSize = textSize * 0.85f + itemView.timeSource.textSize = textSize * 0.85f + translateLabelView.textSize = textSize * 0.85f + translateResultView.textSize = textSize * 1.05f + + itemView.countsUsersHeightHolder.count.textSize = textSize * 1.25f + itemView.countsUsersHeightHolder.label.textSize = textSize * 0.85f + + nameView.nameFirst = adapter.nameFirst + itemView.quotedName.nameFirst = adapter.nameFirst + + itemView.mediaPreview.style = adapter.mediaPreviewStyle + itemView.quotedMediaPreview.style = adapter.mediaPreviewStyle + + itemView.text.customSelectionActionModeCallback = StatusActionModeCallback(itemView.text, activity) + itemView.profileImage.style = adapter.profileImageStyle + + val layoutManager = LinearLayoutManager(adapter.context) + layoutManager.orientation = LinearLayoutManager.HORIZONTAL + itemView.countsUsers.layoutManager = layoutManager + + val countsUsersAdapter = CountsUsersAdapter(fragment, adapter) + itemView.countsUsers.adapter = countsUsersAdapter + val resources = activity.resources + itemView.countsUsers.addItemDecoration(SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.element_spacing_normal))) + + // Apply font families + nameView.applyFontFamily(adapter.lightFont) + summaryView.applyFontFamily(adapter.lightFont) + textView.applyFontFamily(adapter.lightFont) + itemView.quotedName.applyFontFamily(adapter.lightFont) + itemView.quotedText.applyFontFamily(adapter.lightFont) + itemView.locationView.applyFontFamily(adapter.lightFont) + translateLabelView.applyFontFamily(adapter.lightFont) + translateResultView.applyFontFamily(adapter.lightFont) + } + + + private class CountsUsersAdapter( + private val fragment: StatusFragment, + private val statusAdapter: StatusDetailsAdapter + ) : BaseRecyclerViewAdapter(statusAdapter.context, Glide.with(fragment)) { + + private val inflater = LayoutInflater.from(statusAdapter.context) + + private var counts: List? = null + private var users: List? = null + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder.itemViewType) { + ITEM_VIEW_TYPE_USER -> { + (holder as ProfileImageViewHolder).displayUser(getUser(position)!!) + } + ITEM_VIEW_TYPE_COUNT -> { + (holder as CountViewHolder).displayCount(getCount(position)!!) + } + } + } + + private fun getCount(position: Int): LabeledCount? { + if (counts == null) return null + if (position < countItemsCount) { + return counts!![position] + } + return null + } + + override fun getItemCount(): Int { + return countItemsCount + usersCount + } + + + override fun getItemViewType(position: Int): Int { + val countItemsCount = countItemsCount + if (position < countItemsCount) { + return ITEM_VIEW_TYPE_COUNT + } + return ITEM_VIEW_TYPE_USER + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + when (viewType) { + ITEM_VIEW_TYPE_USER -> return ProfileImageViewHolder(this, inflater.inflate(R.layout.adapter_item_status_interact_user, parent, false)) + ITEM_VIEW_TYPE_COUNT -> return CountViewHolder(this, inflater.inflate(R.layout.adapter_item_status_count_label, parent, false)) + } + throw UnsupportedOperationException("Unsupported viewType " + viewType) + } + + fun setUsers(users: List?) { + this.users = users + notifyDataSetChanged() + } + + + fun setCounts(activity: StatusFragment.StatusActivity?) { + if (activity != null) { + val counts = ArrayList() + val replyCount = activity.replyCount + if (replyCount > 0) { + counts.add(LabeledCount(KEY_REPLY_COUNT, replyCount)) + } + val retweetCount = activity.retweetCount + if (retweetCount > 0) { + counts.add(LabeledCount(KEY_RETWEET_COUNT, retweetCount)) + } + val favoriteCount = activity.favoriteCount + if (favoriteCount > 0) { + counts.add(LabeledCount(KEY_FAVORITE_COUNT, favoriteCount)) + } + this.counts = counts + } else { + counts = null + } + notifyDataSetChanged() + } + + fun setCounts(status: ParcelableStatus?) { + if (status != null) { + val counts = ArrayList() + if (status.reply_count > 0) { + counts.add(LabeledCount(KEY_REPLY_COUNT, status.reply_count)) + } + if (status.retweet_count > 0) { + counts.add(LabeledCount(KEY_RETWEET_COUNT, status.retweet_count)) + } + if (status.favorite_count > 0) { + counts.add(LabeledCount(KEY_FAVORITE_COUNT, status.favorite_count)) + } + this.counts = counts + } else { + counts = null + } + notifyDataSetChanged() + } + + val countItemsCount: Int + get() { + if (counts == null) return 0 + return counts!!.size + } + + private val usersCount: Int + get() { + if (users == null) return 0 + return users!!.size + } + + private fun notifyItemClick(position: Int) { + when (getItemViewType(position)) { + ITEM_VIEW_TYPE_COUNT -> { + val count = getCount(position) + val status = statusAdapter.status + if (count == null || status == null) return + when (count.type) { + KEY_RETWEET_COUNT -> { + IntentUtils.openStatusRetweeters(context, status.account_key, + status.originalId) + } + KEY_FAVORITE_COUNT -> { + IntentUtils.openStatusFavoriters(context, status.account_key, + status.originalId) + } + } + } + ITEM_VIEW_TYPE_USER -> { + fragment.onUserClick(getUser(position)!!) + } + } + } + + private fun getUser(position: Int): ParcelableUser? { + val countItemsCount = countItemsCount + if (users == null || position < countItemsCount) return null + return users!![position - countItemsCount] + } + + + internal class ProfileImageViewHolder(private val adapter: CountsUsersAdapter, itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + private val profileImageView = itemView.findViewById(R.id.profileImage) + + init { + itemView.setOnClickListener(this) + } + + fun displayUser(item: ParcelableUser) { + val context = adapter.context + val requestManager = adapter.requestManager + requestManager.loadProfileImage(context, item, adapter.profileImageStyle, + profileImageView.cornerRadius, profileImageView.cornerRadiusRatio, + adapter.profileImageSize).into(profileImageView) + } + + override fun onClick(v: View) { + adapter.notifyItemClick(layoutPosition) + } + } + + internal class CountViewHolder( + private val adapter: CountsUsersAdapter, + itemView: View + ) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + + init { + itemView.setOnClickListener(this) + val textSize = adapter.textSize + itemView.count.textSize = textSize * 1.25f + itemView.label.textSize = textSize * 0.85f + } + + override fun onClick(v: View) { + adapter.notifyItemClick(layoutPosition) + } + + fun displayCount(count: LabeledCount) { + val label: String + when (count.type) { + KEY_REPLY_COUNT -> { + label = adapter.context.getString(R.string.replies) + } + KEY_RETWEET_COUNT -> { + label = adapter.context.getString(R.string.count_label_retweets) + } + KEY_FAVORITE_COUNT -> { + label = adapter.context.getString(R.string.title_favorites) + } + else -> { + throw UnsupportedOperationException("Unsupported type " + count.type) + } + } + itemView.count.text = Utils.getLocalizedNumber(Locale.getDefault(), count.count) + itemView.label.text = label + } + } + + internal class LabeledCount(var type: Int, var count: Long) + + companion object { + private val ITEM_VIEW_TYPE_USER = 1 + private val ITEM_VIEW_TYPE_COUNT = 2 + + private val KEY_REPLY_COUNT = 1 + private val KEY_RETWEET_COUNT = 2 + private val KEY_FAVORITE_COUNT = 3 + } + } + + private class DetailStatusLinkClickHandler( + context: Context, + manager: MultiSelectManager, + private val adapter: StatusDetailsAdapter, + preferences: SharedPreferences + ) : StatusLinkClickHandler(context, manager, preferences) { + + override fun onLinkClick(link: String, orig: String?, accountKey: UserKey?, + extraId: Long, type: Int, sensitive: Boolean, start: Int, end: Int): Boolean { + val position = extraId.toInt() + val current = getCurrentMedia(link, position) + if (current != null && !current.open_browser) { + expandOrOpenMedia(current) + return true + } + return super.onLinkClick(link, orig, accountKey, extraId, type, sensitive, start, end) + } + + private fun expandOrOpenMedia(current: ParcelableMedia) { + if (adapter.isDetailMediaExpanded) { + IntentUtils.openMedia(adapter.context, adapter.status!!, current, + preferences[newDocumentApiKey], preferences[displaySensitiveContentsKey]) + return + } + adapter.isDetailMediaExpanded = true + } + + override fun isMedia(link: String, extraId: Long): Boolean { + val current = getCurrentMedia(link, extraId.toInt()) + return current != null && !current.open_browser + } + + private fun getCurrentMedia(link: String, extraId: Int): ParcelableMedia? { + val status = adapter.getStatus(extraId) + val media = ParcelableMediaUtils.getAllMedia(status) + return findByLink(media, link) + } + } + + private class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) { + if (ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL) { + outRect.set(spacing, 0, 0, 0) + } else { + outRect.set(0, 0, spacing, 0) + } + } + } +} \ No newline at end of file diff --git a/twidere/src/main/res/layout/header_status.xml b/twidere/src/main/res/layout/header_status.xml index a0379df35..78e2a9b10 100644 --- a/twidere/src/main/res/layout/header_status.xml +++ b/twidere/src/main/res/layout/header_status.xml @@ -183,18 +183,23 @@ tools:text="@string/sample_status_text" tools:visibility="visible"/> - + android:visibility="gone" + tools:visibility="visible"> - + + + Cancel retweet Center + Change language Clear Clear messages Comment @@ -687,7 +688,7 @@ Account permission is required No account selected No user selected - Only available when streaming is on, and not 100% reliable. + Only available when streaming is on, and not 100% reliable. Press again to close Profile background image updated Profile banner image updated @@ -1121,7 +1122,7 @@ Retweet - Users turned "Notifications" on, not 100% reliable. + Users turned "Notifications" on, not 100% reliable. Home Interactions Messages diff --git a/twidere/src/main/res/xml/preferences_content.xml b/twidere/src/main/res/xml/preferences_content.xml index fd6cddc70..60be9b16a 100644 --- a/twidere/src/main/res/xml/preferences_content.xml +++ b/twidere/src/main/res/xml/preferences_content.xml @@ -47,9 +47,6 @@ android:summary="@string/preference_summary_trends_location" android:title="@string/preference_title_trends_location"/> -