/* * 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.AsyncTask import android.os.Bundle import android.support.annotation.UiThread import android.support.v4.app.FragmentManagerAccessor import android.support.v4.app.LoaderManager.LoaderCallbacks import android.support.v4.app.hasRunningLoadersSafe import android.support.v4.content.AsyncTaskLoader import android.support.v4.content.ContextCompat 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.LayoutParams 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.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.URLSpan import android.view.* import android.view.View.OnClickListener import android.widget.ImageView import android.widget.Space import android.widget.TextView import com.squareup.otto.Subscribe import edu.tsinghua.hotmobi.HotMobiLogger import edu.tsinghua.hotmobi.model.MediaEvent import edu.tsinghua.hotmobi.model.TimelineType import edu.tsinghua.hotmobi.model.TranslateEvent import edu.tsinghua.hotmobi.model.TweetEvent 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_common.view.* import kotlinx.android.synthetic.main.layout_content_fragment_common.* import org.mariotaku.kpreferences.get import org.mariotaku.ktextension.findPositionByItemId 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.DividerItemDecoration import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter 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.Referral import org.mariotaku.twidere.constant.* import org.mariotaku.twidere.constant.KeyboardShortcutConstants.* import org.mariotaku.twidere.extension.model.getAccountType import org.mariotaku.twidere.extension.model.media_type import org.mariotaku.twidere.loader.ConversationLoader import org.mariotaku.twidere.loader.ParcelableStatusLoader 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.message.FavoriteTaskEvent import org.mariotaku.twidere.model.message.StatusListChangedEvent import org.mariotaku.twidere.model.util.* import org.mariotaku.twidere.provider.TwidereDataStore.* 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.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.util.* /** * Displays status details * Created by mariotaku on 14/12/5. */ class StatusFragment : BaseFragment(), LoaderCallbacks>, OnMediaClickListener, StatusClickListener, KeyboardShortcutCallback, ContentListSupport { private var mItemDecoration: DividerItemDecoration? = 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 mActivityLoaderInitialized: Boolean = false private var hasMoreConversation = true private var statusEvent: TweetEvent? = null // 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 maxId = args.getString(EXTRA_MAX_ID) val sinceId = args.getString(EXTRA_SINCE_ID) val maxSortId = args.getLong(EXTRA_MAX_SORT_ID) val sinceSortId = args.getLong(EXTRA_SINCE_SORT_ID) val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false) val loader = ConversationLoader(activity, status, sinceId, maxId, sinceSortId, maxSortId, adapter.getData(), true, loadingMore) // Setting comparator to null lets statuses sort ascending loader.comparator = null return loader } 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()) { if (conversationLoader.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) val canLoadAllReplies = loader.canLoadAllReplies() if (canLoadAllReplies) { adapter.setReplyError(null) } else { val error = SpannableStringBuilder.valueOf( HtmlSpanBuilder.fromHtml(getString(R.string.cant_load_all_replies_message))) val dialogSpan: ClickableSpan? = error.getSpans(0, error.length, URLSpan::class.java) .firstOrNull { "#dialog" == it.url } if (dialogSpan != null) { val spanStart = error.getSpanStart(dialogSpan) val spanEnd = error.getSpanEnd(dialogSpan) error.removeSpan(dialogSpan) error.setSpan(object : ClickableSpan() { override fun onClick(widget: View) { val activity = activity if (activity == null || activity.isFinishing) return MessageDialogFragment.show(activity.supportFragmentManager, message = getString(R.string.cant_load_all_replies_explanation), tag = "cant_load_all_replies_explanation") } }, spanStart, spanEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE) } adapter.setReplyError(error) } 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) } } } } 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, media: ParcelableMedia, statusPosition: Int) { val status = adapter.getStatus(statusPosition) ?: return IntentUtils.openMedia(activity, status, media, preferences[newDocumentApiKey], preferences[displaySensitiveContentsKey]) val event = MediaEvent.create(activity, status, media, TimelineType.DETAILS, adapter.mediaPreviewEnabled) HotMobiLogger.getInstance(activity).log(status.account_key, event) } override fun onGapClick(holder: GapViewHolder, position: Int) { } override fun onItemActionClick(holder: ViewHolder, id: Int, position: Int) { val status = adapter.getStatus(position) AbsStatusesFragment.handleStatusActionClick(context, fragmentManager, twitterWrapper, holder as StatusViewHolder, status, id) } override fun onStatusClick(holder: IStatusViewHolder, position: Int) { val status = adapter.getStatus(position) ?: return IntentUtils.openStatus(activity, status) } override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) { val status = adapter.getStatus(position) ?: return IntentUtils.openStatus(activity, status.account_key, status.quoted_id) } 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, preferences.getBoolean(KEY_NEW_DOCUMENT_API), Referral.TIMELINE_STATUS, null) } override fun onMediaClick(view: View, media: ParcelableMedia?, accountKey: UserKey, extraId: Long) { val status = adapter.status if (status == null || media == null) return IntentUtils.openMediaDirectly(activity, accountKey, status, media, preferences.getBoolean(KEY_NEW_DOCUMENT_API), null) // BEGIN HotMobi val event = MediaEvent.create(activity, status, media, TimelineType.OTHER, adapter.mediaPreviewEnabled) HotMobiLogger.getInstance(activity).log(status.account_key, event) // END HotMobi } 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) ?: return false val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: return false when (action) { ACTION_STATUS_REPLY -> { val intent = Intent(INTENT_ACTION_REPLY) intent.putExtra(EXTRA_STATUS, status) startActivity(intent) return true } ACTION_STATUS_RETWEET -> { RetweetQuoteDialogFragment.show(fragmentManager, status) return true } ACTION_STATUS_FAVORITE -> { val twitter = twitterWrapper if (status.is_favorite) { twitter.destroyFavoriteAsync(status.account_key, status.id) } else { twitter.createFavoriteAsync(status.account_key, status.id) } return true } } return false } 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) } val event = TweetEvent.create(activity, status, TimelineType.OTHER) event.action = TweetEvent.Action.OPEN if (details != null) { event.isHasTranslateFeature = Utils.isOfficialCredentials(context, details) } else { event.isHasTranslateFeature = false } statusEvent = event Analyzer.log(StatusView(details?.type, status.media_type).apply { this.type = StatusView.getStatusType(status) this.source = HtmlEscapeHelper.toPlainText(status.source) }) } 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.text = errorInfo.message errorIcon.setImageResource(errorInfo.icon) } activity.supportInvalidateOptionsMenu() } override fun onLoaderReset(loader: Loader>) { val event = statusEvent ?: return event.markEnd() val accountKey = UserKey(event.accountId, event.accountHost) HotMobiLogger.getInstance(activity).log(accountKey, event) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.menu_status, menu) } override fun onPrepareOptionsMenu(menu: Menu) { MenuUtils.setItemAvailability(menu, R.id.current_status, adapter.status != null) super.onPrepareOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.current_status -> { if (adapter.status != null) { val position = adapter.getFirstPositionOfItem(StatusAdapter.ITEM_IDX_STATUS) recyclerView.smoothScrollToPosition(position) } return true } } return super.onOptionsItemSelected(item) } private fun setConversation(data: List?) { val readPosition = saveReadPosition() val changed = adapter.setData(data) hasMoreConversation = data != null && changed restoreReadPosition(readPosition) } override val refreshing: Boolean get() = loaderManager.hasRunningLoadersSafe() override fun onLoadMoreContents(@IndicatorPosition position: Long) { if (!hasMoreConversation) return if (position and ILoadMoreSupportAdapter.START !== 0L) { val start = adapter.getIndexStart(StatusAdapter.ITEM_IDX_CONVERSATION) val status = adapter.getStatus(start) if (status == null || status.in_reply_to_status_id == null) return loadConversation(status, null, status.id) } else if (position and ILoadMoreSupportAdapter.END !== 0L) { val start = adapter.getIndexStart(StatusAdapter.ITEM_IDX_CONVERSATION) val status = adapter.getStatus(start + adapter.statusCount - 1) ?: return loadConversation(status, status.id, null) } adapter.loadMoreIndicatorPosition = position } override fun setControlVisible(visible: Boolean) { // No-op } override val reachingEnd: Boolean get() = layoutManager.findLastCompletelyVisibleItemPosition() >= adapter.itemCount - 1 override val reachingStart: Boolean get() = layoutManager.findFirstCompletelyVisibleItemPosition() <= 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() args.putParcelable(EXTRA_ACCOUNT_KEY, status.account_key) args.putString(EXTRA_STATUS_ID, if (status.is_retweet) status.retweet_id else status.id) args.putString(EXTRA_SINCE_ID, sinceId) args.putString(EXTRA_MAX_ID, maxId) args.putParcelable(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() args.putParcelable(EXTRA_ACCOUNT_KEY, status.account_key) args.putString(EXTRA_STATUS_ID, if (status.is_retweet) status.retweet_id else status.id) if (mActivityLoaderInitialized) { loaderManager.restartLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback) return } loaderManager.initLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback) mActivityLoaderInitialized = true } private fun loadTranslation(status: ParcelableStatus?) { if (status == null) return if (AsyncTaskUtils.isTaskRunning(loadTranslationTask)) { loadTranslationTask!!.cancel(true) } loadTranslationTask = LoadTranslationTask(this) AsyncTaskUtils.executeTask(loadTranslationTask, status) } private fun displayTranslation(translation: TranslationResult) { adapter.translationResult = translation val status = this.status val context = this.context ?: return if (status != null) { val event = TranslateEvent.create(context, status, translation.translatedLang) HotMobiLogger.getInstance(context).log(status.account_key, event) } } 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 //TODO maintain read position 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 } private fun showConversationError(exception: Exception) { } 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) ?: return val inflater = MenuInflater(context) inflater.inflate(R.menu.action_status, menu) MenuUtils.setupForStatus(context, preferences, menu, status, twitterWrapper, userColorNameManager) } 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) ?: return false 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, 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, Referral.TIMELINE_STATUS, 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) return builder.create() } } internal class LoadTranslationTask(val fragment: StatusFragment) : AsyncTask>() { val context: Context init { context = fragment.activity } override fun doInBackground(vararg params: ParcelableStatus): SingleResponse { val status = params[0] val twitter = MicroBlogAPIFactory.getInstance(context, status.account_key ) val prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) if (twitter == null) return SingleResponse.Companion.getInstance() try { val prefDest = prefs.getString(SharedPreferenceConstants.KEY_TRANSLATION_DESTINATION, null) val dest: String if (TextUtils.isEmpty(prefDest)) { dest = twitter.accountSettings.language val editor = prefs.edit() editor.putString(SharedPreferenceConstants.KEY_TRANSLATION_DESTINATION, dest) editor.apply() } else { dest = prefDest } val statusId = if (status.is_retweet) status.retweet_id else status.id return SingleResponse.Companion.getInstance(twitter.showTranslation(statusId, dest)) } catch (e: MicroBlogException) { return SingleResponse.Companion.getInstance(e) } } override fun onPostExecute(result: SingleResponse) { if (result.data != null) { fragment.displayTranslation(result.data) } else if (result.hasException()) { Utils.showErrorMessage(context, R.string.translate, result.exception, false) } } } private class DetailStatusViewHolder( private val adapter: StatusAdapter, itemView: View ) : ViewHolder(itemView), OnClickListener, ActionMenuView.OnMenuItemClickListener { private val linkClickHandler: StatusLinkClickHandler private val linkify: TwidereLinkify private val locationView: TextView private val retweetedByView: TextView init { this.linkClickHandler = DetailStatusLinkClickHandler(adapter.context, adapter.multiSelectManager, adapter, adapter.preferences) this.linkify = TwidereLinkify(linkClickHandler) locationView = itemView.locationView retweetedByView = itemView.retweetedBy 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 loader = adapter.mediaLoader 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_screen_name, nameFirst) retweetedByView.text = context.getString(R.string.name_retweeted, retweetedBy) retweetedByView.visibility = View.VISIBLE } else { retweetedByView.text = null retweetedByView.visibility = View.GONE } itemView.profileContainer.drawEnd(status.account_color) val layoutPosition = layoutPosition val skipLinksInText = status.extras != null && status.extras.support_entities if (status.is_quote) { itemView.quotedView.visibility = View.VISIBLE val originalIdAvailable = !TextUtils.isEmpty(status.quoted_id) 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.setName(colorNameManager.getUserNickname(status.quoted_user_key!!, status.quoted_user_name)) itemView.quotedName.setScreenName(String.format("@%s", status.quoted_user_screen_name)) itemView.quotedName.updateText(formatter) var quotedDisplayEnd = -1 if (status.extras.quoted_display_text_range != null) { quotedDisplayEnd = status.extras.quoted_display_text_range!![1] } val quotedText = SpannableStringBuilder.valueOf(status.quoted_text_unescaped) ParcelableStatusUtils.applySpans(quotedText, status.quoted_spans) linkify.applyAllLinks(quotedText, status.account_key, layoutPosition.toLong(), status.is_possibly_sensitive, skipLinksInText) if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) { itemView.quotedText.text = quotedText.subSequence(0, quotedDisplayEnd) } else { itemView.quotedText.text = quotedText } if (itemView.quotedText.length() == 0) { // No text itemView.quotedText.visibility = View.GONE } else { itemView.quotedText.visibility = View.VISIBLE } itemView.quoteIndicator.color = colorNameManager.getUserColor(status.quoted_user_key!!) val quotedMedia = status.quoted_media if (quotedMedia?.isEmpty() ?: true) { itemView.quotedMediaPreviewContainer.visibility = View.GONE itemView.quotedMediaPreview.visibility = View.GONE itemView.quotedMediaPreviewPlaceholder.visibility = View.GONE } else if (adapter.isDetailMediaExpanded) { itemView.quotedMediaPreviewContainer.visibility = View.VISIBLE itemView.quotedMediaPreview.visibility = View.VISIBLE itemView.quotedMediaPreviewPlaceholder.visibility = View.GONE itemView.quotedMediaPreview.displayMedia(quotedMedia, loader, status.account_key, -1, adapter.fragment, null) } else { itemView.quotedMediaPreviewContainer.visibility = View.VISIBLE itemView.quotedMediaPreview.visibility = View.GONE itemView.quotedMediaPreviewPlaceholder.visibility = View.VISIBLE } } else { itemView.quotedName.visibility = View.GONE itemView.quotedText.visibility = View.VISIBLE itemView.quotedMediaPreviewContainer.visibility = View.GONE // Not available val string = SpannableString.valueOf(context.getString(R.string.status_not_available_text)) string.setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context, android.R.attr.textColorTertiary, itemView.text.currentTextColor)), 0, string.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) itemView.quotedText.text = string itemView.quoteIndicator.color = 0 } } 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 } itemView.name.setName(colorNameManager.getUserNickname(status.user_key, status.user_name)) itemView.name.setScreenName(String.format("@%s", status.user_screen_name)) itemView.name.updateText(formatter) loader.displayProfileImage(itemView.profileImage, status) 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) { itemView.profileType.setImageResource(typeIconRes) itemView.profileType.contentDescription = context.getString(typeDescriptionRes) itemView.profileType.visibility = View.VISIBLE } else { itemView.profileType.setImageDrawable(null) itemView.profileType.contentDescription = null itemView.profileType.visibility = View.GONE } val timeString = Utils.formatToLongTimeString(context, timestamp) if (!TextUtils.isEmpty(timeString) && !TextUtils.isEmpty(status.source)) { itemView.timeSource.text = HtmlSpanBuilder.fromHtml(context.getString(R.string.status_format_time_source, timeString, status.source)) } else if (TextUtils.isEmpty(timeString) && !TextUtils.isEmpty(status.source)) { itemView.timeSource.text = HtmlSpanBuilder.fromHtml(status.source) } else if (!TextUtils.isEmpty(timeString) && TextUtils.isEmpty(status.source)) { itemView.timeSource.text = timeString } itemView.timeSource.movementMethod = LinkMovementMethod.getInstance() var displayEnd = -1 if (status.extras.display_text_range != null) { displayEnd = status.extras.display_text_range!![1] } val text = SpannableStringBuilder.valueOf(status.text_unescaped) ParcelableStatusUtils.applySpans(text, status.spans) linkify.applyAllLinks(text, status.account_key, layoutPosition.toLong(), status.is_possibly_sensitive, skipLinksInText) if (displayEnd != -1 && displayEnd <= text.length) { itemView.text.text = text.subSequence(0, displayEnd) } else { itemView.text.text = text } if (itemView.text.length() == 0) { // No text itemView.text.visibility = View.GONE } else { itemView.text.visibility = View.VISIBLE } val location: ParcelableLocation? = status.location val placeFullName: String? = status.place_full_name if (!TextUtils.isEmpty(placeFullName)) { locationView.visibility = View.VISIBLE locationView.text = 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.text = null } val interactUsersAdapter = itemView.countsUsers.adapter as CountsUsersAdapter if (statusActivity != null) { interactUsersAdapter.setUsers(statusActivity.retweeters) interactUsersAdapter.setCounts(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(media, loader, status.account_key, -1, adapter.fragment, adapter.mediaLoadingHandler) } 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!!) itemView.twitterCard.visibility = View.VISIBLE if (size != null) { itemView.twitterCard.setCardSize(size.x, size.y) } else { itemView.twitterCard.setCardSize(0, 0) } val cardFragment = TwitterCardFragmentFactory.createCardFragment(status) val fm = fragment.childFragmentManager if (cardFragment != null && !FragmentManagerAccessor.isStateSaved(fm)) { val ft = fm.beginTransaction() ft.replace(R.id.twitterCard, cardFragment) ft.commit() } else { itemView.twitterCard.visibility = View.GONE } } else { itemView.twitterCard.visibility = View.GONE } MenuUtils.setupForStatus(context, fragment.preferences, itemView.menuBar.menu, status, adapter.statusAccount!!, twitter, colorNameManager) val lang = status.lang if (!Utils.isOfficialCredentials(context, account) || !CheckUtils.isValidLocale(lang)) { itemView.translateLabel.setText(R.string.unknown_language) itemView.translateContainer.visibility = View.GONE } else { val locale = Locale(lang) itemView.translateContainer.visibility = View.VISIBLE if (translation != null) { itemView.translateLabel.text = context.getString(R.string.translation) itemView.translateResult.visibility = View.VISIBLE itemView.translateResult.text = translation.text } else { itemView.translateLabel.text = context.getString(R.string.translate_from_language, locale.displayLanguage) itemView.translateResult.visibility = View.GONE } } itemView.text.setTextIsSelectable(true) itemView.translateResult.setTextIsSelectable(true) itemView.text.movementMethod = LinkMovementMethod.getInstance() itemView.quotedText.movementMethod = null } override fun onClick(v: View) { val status = adapter.getStatus(layoutPosition) ?: return 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, preferences.getBoolean(KEY_NEW_DOCUMENT_API), Referral.STATUS, 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, preferences.getBoolean(KEY_NEW_DOCUMENT_API), Referral.STATUS, null) } } locationView -> { val location = status.location if (!ParcelableLocationUtils.isValidLocation(location)) return IntentUtils.openMap(adapter.context, location.latitude, location.longitude) } itemView.quotedView -> { IntentUtils.openStatus(adapter.context, status.account_key, status.quoted_id) } 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) ?: return false val twitter = fragment.twitterWrapper val manager = fragment.userColorNameManager val activity = fragment.activity val fm = fragment.fragmentManager if (item.itemId == R.id.retweet) { RetweetQuoteDialogFragment.show(fm, status) return true } return MenuUtils.handleStatusClick(activity, fragment, fm, manager, twitter, status, item) } private fun initViews() { // menuBar.setOnMenuItemClickListener(this); 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, 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 itemView.name.setPrimaryTextSize(textSize * 1.25f) itemView.name.setSecondaryTextSize(textSize * 0.85f) itemView.text.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 itemView.name.setNameFirst(adapter.nameFirst) itemView.quotedName.setNameFirst(adapter.nameFirst) itemView.mediaPreview.setStyle(adapter.mediaPreviewStyle) itemView.quotedMediaPreview.setStyle(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))) } private class CountsUsersAdapter( private val fragment: StatusFragment, private val statusAdapter: StatusAdapter ) : BaseRecyclerViewAdapter(statusAdapter.context) { private val inflater: LayoutInflater private var counts: List? = null private var users: List? = null init { inflater = LayoutInflater.from(statusAdapter.context) } 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 -> { if (status.is_retweet) { IntentUtils.openStatusRetweeters(context, status.account_key, status.retweet_id) } else { IntentUtils.openStatusRetweeters(context, status.account_key, status.id) } } KEY_FAVORITE_COUNT -> { val account = statusAdapter.statusAccount ?: return if (!Utils.isOfficialCredentials(context, account)) return if (status.is_retweet) { IntentUtils.openStatusFavoriters(context, status.account_key, status.retweet_id) } else { IntentUtils.openStatusFavoriters(context, status.account_key, status.id) } } } } 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: ImageView init { itemView.setOnClickListener(this) profileImageView = itemView.findViewById(R.id.profileImage) as ImageView } fun displayUser(item: ParcelableUser) { adapter.mediaLoader.displayProfileImage(profileImageView, item) } 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 current = getCurrentMedia(link, extraId.toInt()) 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), IStatusesAdapter> { private val inflater: LayoutInflater override val mediaLoadingHandler: MediaLoadingHandler override val twidereLinkify: TwidereLinkify override var statusClickListener: StatusClickListener? = null private var recyclerView: RecyclerView? = null private var statusViewHolder: DetailStatusViewHolder? = null private val itemCounts: IntArray override val nameFirst: Boolean private val cardBackgroundColor: Int override val mediaPreviewStyle: Int override val linkHighlightingStyle: Int override val mediaPreviewEnabled: Boolean override val sensitiveContentEnabled: Boolean private val mShowCardActions: Boolean override val useStarsForLikes: Boolean private var mDetailMediaExpanded: Boolean = false var status: ParcelableStatus? = null internal set var translationResult: TranslationResult? = null internal set(translation) { if (status == null || translation == null || !TextUtils.equals(InternalTwitterContentUtils.getOriginalId(status!!), 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 notifyDataSetChanged() } var statusAccount: AccountDetails? = null internal set private var data: List? = null private var replyError: CharSequence? = null private var conversationError: CharSequence? = null private var mReplyStart: Int = 0 private var mShowingActionCardPosition: Int = 0 init { setHasStableIds(true) val context = fragment.activity itemCounts = IntArray(ITEM_TYPES_SUM) // 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) mediaLoadingHandler = MediaLoadingHandler(R.id.media_preview_progress) cardBackgroundColor = ThemeUtils.getCardBackgroundColor(context, ThemeUtils.getThemeBackgroundOption(context), ThemeUtils.getUserThemeBackgroundAlpha(context)) nameFirst = preferences[nameFirstKey] mediaPreviewStyle = preferences[mediaPreviewStyleKey] linkHighlightingStyle = preferences[linkHighlightOptionKey] mediaPreviewEnabled = preferences[mediaPreviewKey] sensitiveContentEnabled = preferences.getBoolean(SharedPreferenceConstants.KEY_DISPLAY_SENSITIVE_CONTENTS, false) mShowCardActions = !preferences[hideCardActionsKey] useStarsForLikes = preferences[iWantMyStarsBackKey] val listener = StatusAdapterLinkClickHandler>(context, preferences) listener.setAdapter(this) twidereLinkify = TwidereLinkify(listener) } override fun getStatus(position: Int): ParcelableStatus? { val itemType = getItemType(position) when (itemType) { ITEM_IDX_CONVERSATION -> { if (data == null) return null return data!![position - getIndexStart(ITEM_IDX_CONVERSATION)] } ITEM_IDX_REPLY -> { if (data == null || mReplyStart < 0) return null return data!![position - getIndexStart(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_STATUS) + mReplyStart] } ITEM_IDX_STATUS -> { return status } } return null } fun getIndexStart(index: Int): Int { if (index == 0) return 0 return TwidereMathUtils.sum(itemCounts, 0, index - 1) } override fun getStatusId(position: Int): String? { val status = getStatus(position) return status?.id } override fun getStatusTimestamp(position: Int): Long { val status = getStatus(position) return status?.timestamp ?: -1 } override fun getStatusPositionKey(position: Int): Long { val status = getStatus(position) ?: return -1 return if (status.position_key > 0) status.timestamp else getStatusTimestamp(position) } override fun getAccountKey(position: Int): UserKey? { val status = getStatus(position) return status?.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 val statusCount: Int get() = rawStatusCount override val rawStatusCount: Int get() { 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 mShowCardActions return mShowCardActions || mShowingActionCardPosition == position } override fun showCardActions(position: Int) { if (mShowingActionCardPosition != RecyclerView.NO_POSITION) { notifyItemChanged(mShowingActionCardPosition) } mShowingActionCardPosition = position if (position != RecyclerView.NO_POSITION) { notifyItemChanged(position) } } override fun setData(data: List?): Boolean { val status = this.status ?: return false val changed = !CompareUtils.objectEquals(data, data) this.data = data if (data == null || data.isEmpty()) { setTypeCount(ITEM_IDX_CONVERSATION, 0) setTypeCount(ITEM_IDX_REPLY, 0) mReplyStart = -1 } else { var sortId = status.sort_id if (status.is_retweet) { for (item in data) { if (TextUtils.equals(status.retweet_id, item.id)) { sortId = item.sort_id break } } } var conversationCount = 0 var replyCount = 0 var replyStart = -1 for (i in 0 until data.size) { val item = data[i] if (item.sort_id < sortId) { conversationCount++ } else if (item.sort_id > sortId && status.id != item.id) { if (replyStart < 0) { replyStart = i } replyCount++ } } setTypeCount(ITEM_IDX_CONVERSATION, conversationCount) setTypeCount(ITEM_IDX_REPLY, replyCount) mReplyStart = replyStart } notifyDataSetChanged() updateItemDecoration() return changed } override val showAccountsColor: Boolean get() = false var isDetailMediaExpanded: Boolean get() { if (mDetailMediaExpanded) return true if (mediaPreviewEnabled) { val status = this.status return status != null && (sensitiveContentEnabled || !status.is_possibly_sensitive) } return false } set(expanded) { mDetailMediaExpanded = 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 -> { if (statusViewHolder != null) { return statusViewHolder } val view = inflater.inflate(R.layout.header_status_compact, parent, false) val cardView = view.findViewById(R.id.compact_card) cardView.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.card_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) { 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) statusHolder.displayStatus(status!!, itemType == ITEM_IDX_CONVERSATION && position - getItemTypeStart(position) == 0) } 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 onViewDetachedFromWindow(holder: ViewHolder?) { if (holder is DetailStatusViewHolder) { statusViewHolder = holder as DetailStatusViewHolder? } super.onViewDetachedFromWindow(holder) } override fun onViewAttachedToWindow(holder: ViewHolder?) { if (holder === statusViewHolder) { statusViewHolder = null } super.onViewAttachedToWindow(holder) } 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() } fun getItemType(position: Int): Int { var typeStart = 0 for (type in 0..ITEM_TYPES_SUM - 1) { val typeCount = getTypeCount(type) val typeEnd = typeStart + typeCount if (position >= typeStart && position < typeEnd) return type typeStart = typeEnd } throw IllegalStateException("Unknown position " + position) } fun getItemTypeStart(position: Int): Int { var typeStart = 0 for (type in 0..ITEM_TYPES_SUM - 1) { val typeCount = getTypeCount(type) val typeEnd = typeStart + typeCount if (position >= typeStart && position < typeEnd) return typeStart typeStart = typeEnd } throw IllegalStateException() } override fun getItemId(position: Int): Long { val status = getStatus(position) if (status != null) return status.hashCode().toLong() return getItemType(position).toLong() } override fun getItemCount(): Int { if (status == null) return 0 return TwidereMathUtils.sum(itemCounts) } 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 old = this.status this.status = status statusAccount = account notifyDataSetChanged() updateItemDecoration() return !CompareUtils.objectEquals(old, status) } fun updateItemDecoration() { if (recyclerView == null) return } fun getFirstPositionOfItem(itemIdx: Int): Int { var position = 0 for (i in 0..ITEM_TYPES_SUM - 1) { if (itemIdx == i) return position position += getTypeCount(i) } return RecyclerView.NO_POSITION } fun getData(): List? { return data } var isConversationsLoading: Boolean get() = ILoadMoreSupportAdapter.has(loadMoreIndicatorPosition, ILoadMoreSupportAdapter.START) set(loading) { if (loading) { loadMoreIndicatorPosition = loadMoreIndicatorPosition or ILoadMoreSupportAdapter.START } else { loadMoreIndicatorPosition = loadMoreIndicatorPosition and ILoadMoreSupportAdapter.START.inv() } updateItemDecoration() } var isRepliesLoading: Boolean get() = ILoadMoreSupportAdapter.has(loadMoreIndicatorPosition, ILoadMoreSupportAdapter.END) 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: TextView init { textView = itemView.findViewById(android.R.id.text1) as TextView 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 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 { var heightBeforeSpace = 0 if (getItemViewType(child) == StatusAdapter.VIEW_TYPE_SPACE) { for (i in 0 until childCount) { val childToMeasure = getChildAt(i) val paramsToMeasure = childToMeasure.layoutParams as LayoutParams val typeToMeasure = getItemViewType(childToMeasure) if (typeToMeasure == StatusAdapter.VIEW_TYPE_SPACE) { break } if (typeToMeasure == StatusAdapter.VIEW_TYPE_DETAIL_STATUS || heightBeforeSpace != 0) { heightBeforeSpace += super.getDecoratedMeasuredHeight(childToMeasure) +paramsToMeasure.topMargin + paramsToMeasure.bottomMargin } } if (heightBeforeSpace != 0) { val spaceHeight = recyclerView.measuredHeight - heightBeforeSpace this.spaceHeight = Math.max(0, spaceHeight) return this.spaceHeight } } 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) val top = view.top var height = view.height if (height > 0) { extent += top * 100 / height } view = findViewByPosition(lastPosition) 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) 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 return getDecoratedMeasuredHeight(space) } } class StatusActivitySummaryLoader( context: Context, private val accountKey: UserKey, private val statusId: String ) : AsyncTaskLoader(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()) val retweeters = ArrayList() try { for (status in twitter.getRetweets(statusId, paging)) { val user = ParcelableUserUtils.fromUser(status.user, accountKey) if (!DataStoreUtils.isFilteringUser(context, user.key.toString())) { retweeters.add(user) } } activitySummary.retweeters = retweeters 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.STATUS_ID), Expression.equalsArgs(Statuses.RETWEET_ID))) val statusWhereArgs = arrayOf(accountKey.toString(), statusId, statusId) cr.update(Statuses.CONTENT_URI, countValues, statusWhere.sql, statusWhereArgs) val activityWhere = Expression.and( Expression.equalsArgs(Activities.ACCOUNT_KEY), Expression.or( Expression.equalsArgs(Activities.STATUS_ID), Expression.equalsArgs(Activities.STATUS_RETWEET_ID))) val pStatus = ParcelableStatusUtils.fromStatus(status, accountKey, false) cr.insert(CachedStatuses.CONTENT_URI, ParcelableStatusValuesCreator.create(pStatus)) val activityCursor = cr.query(Activities.AboutMe.CONTENT_URI, Activities.COLUMNS, activityWhere.sql, statusWhereArgs, null)!! try { activityCursor.moveToFirst() val ci = ParcelableActivityCursorIndices(activityCursor) while (!activityCursor.isAfterLast) { val activity = ci.newObject(activityCursor) val activityStatus = activity.getActivityStatus() if (activityStatus != null) { activityStatus.favorite_count = activitySummary.favoriteCount activityStatus.reply_count = activitySummary.replyCount activityStatus.retweet_count = activitySummary.retweetCount } cr.update(Activities.AboutMe.CONTENT_URI, ParcelableActivityValuesCreator.create(activity), Expression.equals(Activities._ID, activity._id).sql, null) activityCursor.moveToNext() } } finally { activityCursor.close() } 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 TextUtils.equals(statusId, if (status.is_retweet) status.retweet_id else status.id) } } data class ReadPosition(var statusId: Long, var offsetTop: Int) private class StatusDividerItemDecoration( context: Context, private val statusAdapter: StatusAdapter, orientation: Int ) : DividerItemDecoration(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 } }