complete link preview

This commit is contained in:
Tlaster 2020-06-02 17:15:34 +08:00
parent cc38f01c5e
commit d345ad3d25
22 changed files with 199 additions and 71 deletions

View File

@ -291,6 +291,8 @@ public interface SharedPreferenceConstants {
String KEY_AUTO_HIDE_TABS = "auto_hide_tabs"; String KEY_AUTO_HIDE_TABS = "auto_hide_tabs";
@ExportablePreference(BOOLEAN) @ExportablePreference(BOOLEAN)
String KEY_HIDE_CARD_NUMBERS = "hide_card_numbers"; String KEY_HIDE_CARD_NUMBERS = "hide_card_numbers";
@ExportablePreference(BOOLEAN)
String KEY_SHOW_LINK_PREVIEW = "show_link_preview";
// Internal preferences // Internal preferences

View File

@ -20,11 +20,11 @@ package org.mariotaku.twidere.model;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.IntDef; import android.text.TextUtils;
import androidx.annotation.LongDef; import androidx.annotation.LongDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.bluelinelabs.logansquare.annotation.JsonField; import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject; import com.bluelinelabs.logansquare.annotation.JsonObject;
@ -527,6 +527,9 @@ public class ParcelableStatus implements Parcelable, Comparable<ParcelableStatus
@JsonField(name = "external_url") @JsonField(name = "external_url")
public String external_url; public String external_url;
@JsonField(name = "entities_url")
public String[] entities_url;
@JsonField(name = "quoted_external_url") @JsonField(name = "quoted_external_url")
public String quoted_external_url; public String quoted_external_url;

View File

@ -230,6 +230,7 @@ dependencies {
implementation "com.google.dagger:dagger:${libVersions['Dagger']}" implementation "com.google.dagger:dagger:${libVersions['Dagger']}"
kapt "com.google.dagger:dagger-compiler:${libVersions['Dagger']}" kapt "com.google.dagger:dagger-compiler:${libVersions['Dagger']}"
implementation 'org.attoparser:attoparser:2.0.4.RELEASE' implementation 'org.attoparser:attoparser:2.0.4.RELEASE'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.9.1' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.9.1'
implementation 'net.ypresto.androidtranscoder:android-transcoder:0.2.0' implementation 'net.ypresto.androidtranscoder:android-transcoder:0.2.0'
implementation "com.google.android.exoplayer:exoplayer-core:${libVersions['Exoplayer']}" implementation "com.google.android.exoplayer:exoplayer-core:${libVersions['Exoplayer']}"

View File

@ -91,6 +91,7 @@ public interface Constants extends TwidereConstants {
String TWIDERE_PREVIEW_NAME = "Twidere Project"; String TWIDERE_PREVIEW_NAME = "Twidere Project";
String TWIDERE_PREVIEW_SCREEN_NAME = "TwidereProject"; String TWIDERE_PREVIEW_SCREEN_NAME = "TwidereProject";
String TWIDERE_PREVIEW_TEXT_HTML = "Twidere is an open source twitter client for Android, see <a href='https://github.com/mariotaku/twidere'>github.com/mariotak&#8230;</a>"; String TWIDERE_PREVIEW_TEXT_HTML = "Twidere is an open source twitter client for Android, see <a href='https://github.com/mariotaku/twidere'>github.com/mariotak&#8230;</a>";
String TWIDERE_PREVIEW_LINK_URI = "https://github.com/TwidereProject/Twidere-Android";
String TWIDERE_PREVIEW_TEXT_UNESCAPED = "Twidere is an open source twitter client for Android, see github.com/mariotak&#8230;"; String TWIDERE_PREVIEW_TEXT_UNESCAPED = "Twidere is an open source twitter client for Android, see github.com/mariotak&#8230;";
String TWIDERE_PREVIEW_SOURCE = "Twidere for Android"; String TWIDERE_PREVIEW_SOURCE = "Twidere for Android";

View File

@ -65,6 +65,8 @@ class DummyItemAdapter(
var showCardNumbers: Boolean = false var showCardNumbers: Boolean = false
var showLinkPreview: Boolean = false
private var showingActionCardPosition = RecyclerView.NO_POSITION private var showingActionCardPosition = RecyclerView.NO_POSITION
private val showingFullTextStates = SparseBooleanArray() private val showingFullTextStates = SparseBooleanArray()
@ -110,6 +112,10 @@ class DummyItemAdapter(
return showCardActions || showingActionCardPosition == position return showCardActions || showingActionCardPosition == position
} }
override fun isLinkPreviewShown(position: Int): Boolean {
return showLinkPreview
}
override fun showCardActions(position: Int) { override fun showCardActions(position: Int) {
if (showingActionCardPosition != RecyclerView.NO_POSITION && adapter != null) { if (showingActionCardPosition != RecyclerView.NO_POSITION && adapter != null) {
adapter.notifyItemChanged(showingActionCardPosition) adapter.notifyItemChanged(showingActionCardPosition)
@ -189,6 +195,7 @@ class DummyItemAdapter(
sensitiveContentEnabled = preferences[displaySensitiveContentsKey] sensitiveContentEnabled = preferences[displaySensitiveContentsKey]
showCardActions = !preferences[hideCardActionsKey] showCardActions = !preferences[hideCardActionsKey]
showCardNumbers = !preferences[hideCardNumbersKey] showCardNumbers = !preferences[hideCardNumbersKey]
showLinkPreview = preferences[showLinkPreviewKey]
linkHighlightingStyle = preferences[linkHighlightOptionKey] linkHighlightingStyle = preferences[linkHighlightOptionKey]
lightFont = preferences[lightFontKey] lightFont = preferences[lightFontKey]
useStarsForLikes = preferences[iWantMyStarsBackKey] useStarsForLikes = preferences[iWantMyStarsBackKey]

View File

@ -21,11 +21,11 @@ package org.mariotaku.twidere.adapter
import android.content.Context import android.content.Context
import android.database.CursorIndexOutOfBoundsException import android.database.CursorIndexOutOfBoundsException
import androidx.legacy.widget.Space
import androidx.recyclerview.widget.RecyclerView
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.legacy.widget.Space
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.* import org.mariotaku.ktextension.*
@ -80,6 +80,7 @@ abstract class ParcelableStatusesAdapter(
final override val sensitiveContentEnabled: Boolean = preferences.getBoolean(KEY_DISPLAY_SENSITIVE_CONTENTS, false) final override val sensitiveContentEnabled: Boolean = preferences.getBoolean(KEY_DISPLAY_SENSITIVE_CONTENTS, false)
private val showCardActions: Boolean = !preferences[hideCardActionsKey] private val showCardActions: Boolean = !preferences[hideCardActionsKey]
private val showCardNumbers: Boolean = !preferences[hideCardNumbersKey] private val showCardNumbers: Boolean = !preferences[hideCardNumbersKey]
private val showLinkPreview: Boolean = preferences[showLinkPreviewKey]
private val gapLoadingIds: MutableSet<ObjectId> = HashSet() private val gapLoadingIds: MutableSet<ObjectId> = HashSet()
@ -267,6 +268,10 @@ abstract class ParcelableStatusesAdapter(
return showCardNumbers || showingActionCardId == getItemId(position) return showCardNumbers || showingActionCardId == getItemId(position)
} }
override fun isLinkPreviewShown(position: Int): Boolean {
return showLinkPreview
}
override fun isCardActionsShown(position: Int): Boolean { override fun isCardActionsShown(position: Int): Boolean {
if (position == RecyclerView.NO_POSITION) return showCardActions if (position == RecyclerView.NO_POSITION) return showCardActions
return showCardActions || showingActionCardId == getItemId(position) return showCardActions || showingActionCardId == getItemId(position)

View File

@ -19,7 +19,6 @@
package org.mariotaku.twidere.adapter package org.mariotaku.twidere.adapter
import androidx.recyclerview.widget.RecyclerView
import android.text.TextUtils import android.text.TextUtils
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
@ -28,6 +27,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Space import android.widget.Space
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.contains import org.mariotaku.ktextension.contains
import org.mariotaku.microblog.library.twitter.model.TranslationResult import org.mariotaku.microblog.library.twitter.model.TranslationResult
@ -72,6 +72,7 @@ class StatusDetailsAdapter(
private val cardBackgroundColor: Int private val cardBackgroundColor: Int
private val showCardActions = !preferences[hideCardActionsKey] private val showCardActions = !preferences[hideCardActionsKey]
private val showCardNumbers = !preferences[hideCardNumbersKey] private val showCardNumbers = !preferences[hideCardNumbersKey]
private val showLinkPreview = preferences[showLinkPreviewKey]
private var recyclerView: RecyclerView? = null private var recyclerView: RecyclerView? = null
private var detailMediaExpanded: Boolean = false private var detailMediaExpanded: Boolean = false
@ -179,6 +180,10 @@ class StatusDetailsAdapter(
return showCardNumbers || showingActionCardPosition == position return showCardNumbers || showingActionCardPosition == position
} }
override fun isLinkPreviewShown(position: Int): Boolean {
return showLinkPreview
}
override fun isCardActionsShown(position: Int): Boolean { override fun isCardActionsShown(position: Int): Boolean {
if (position == RecyclerView.NO_POSITION) return showCardActions if (position == RecyclerView.NO_POSITION) return showCardActions
return showCardActions || showingActionCardPosition == position return showCardActions || showingActionCardPosition == position

View File

@ -35,6 +35,8 @@ interface IStatusesAdapter<in Data> : IContentAdapter, IGapSupportedAdapter {
fun isCardNumbersShown(position: Int): Boolean fun isCardNumbersShown(position: Int): Boolean
fun isLinkPreviewShown(position: Int): Boolean
fun isCardActionsShown(position: Int): Boolean fun isCardActionsShown(position: Int): Boolean
fun showCardActions(position: Int) fun showCardActions(position: Int)

View File

@ -90,6 +90,7 @@ val tabPositionKey = KStringKey(KEY_TAB_POSITION, SharedPreferenceConstants.DEFA
val yandexKeyKey = KStringKey(SharedPreferenceConstants.KEY_YANDEX_KEY, TwidereConstants.YANDEX_KEY) val yandexKeyKey = KStringKey(SharedPreferenceConstants.KEY_YANDEX_KEY, TwidereConstants.YANDEX_KEY)
val autoHideTabs = KBooleanKey(SharedPreferenceConstants.KEY_AUTO_HIDE_TABS, true) val autoHideTabs = KBooleanKey(SharedPreferenceConstants.KEY_AUTO_HIDE_TABS, true)
val hideCardNumbersKey = KBooleanKey(KEY_HIDE_CARD_NUMBERS, false) val hideCardNumbersKey = KBooleanKey(KEY_HIDE_CARD_NUMBERS, false)
val showLinkPreviewKey = KBooleanKey(KEY_SHOW_LINK_PREVIEW, false)
object cacheSizeLimitKey : KSimpleKey<Int>(KEY_CACHE_SIZE_LIMIT, 300) { object cacheSizeLimitKey : KSimpleKey<Int>(KEY_CACHE_SIZE_LIMIT, 300) {

View File

@ -46,6 +46,7 @@ import org.mariotaku.twidere.util.HtmlSpanBuilder
import org.mariotaku.twidere.util.InternalTwitterContentUtils import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.InternalTwitterContentUtils.getMediaUrl import org.mariotaku.twidere.util.InternalTwitterContentUtils.getMediaUrl
import org.mariotaku.twidere.util.InternalTwitterContentUtils.getStartEndForEntity import org.mariotaku.twidere.util.InternalTwitterContentUtils.getStartEndForEntity
import kotlin.math.max
fun Status.toParcelable(details: AccountDetails, profileImageSize: String = "normal", fun Status.toParcelable(details: AccountDetails, profileImageSize: String = "normal",
updateFilterInfoAction: (Status, ParcelableStatus) -> Unit = ::updateFilterInfoDefault): ParcelableStatus { updateFilterInfoAction: (Status, ParcelableStatus) -> Unit = ::updateFilterInfoDefault): ParcelableStatus {
@ -71,6 +72,13 @@ fun Status.applyTo(accountKey: UserKey, accountType: String, profileImageSize: S
result.timestamp = createdAt?.time ?: 0 result.timestamp = createdAt?.time ?: 0
extras.external_url = inferredExternalUrl extras.external_url = inferredExternalUrl
extras.entities_url = entities?.urls?.map { it.expandedUrl }?.let {
if (isQuoteStatus) {
it.take(max(0, it.count() - 1))
} else {
it
}
}?.toTypedArray()
extras.support_entities = entities != null extras.support_entities = entities != null
extras.statusnet_conversation_id = statusnetConversationId extras.statusnet_conversation_id = statusnetConversationId
extras.conversation_id = conversationId extras.conversation_id = conversationId
@ -367,7 +375,7 @@ private inline val Status.inferredExternalUrl
noticeUriRegex.matchEntire(uri)?.let { result: MatchResult -> noticeUriRegex.matchEntire(uri)?.let { result: MatchResult ->
"https://${result.groups[1]?.value}/notice/${result.groups[3]?.value}" "https://${result.groups[1]?.value}/notice/${result.groups[3]?.value}"
} }
} ?: entities?.urls?.firstOrNull()?.expandedUrl }
private val Status.parcelableLocation: ParcelableLocation? private val Status.parcelableLocation: ParcelableLocation?
get() { get() {

View File

@ -25,6 +25,7 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -465,6 +466,12 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
startActivity(intent) startActivity(intent)
} }
override fun onLinkClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)
val url = status.extras?.entities_url?.firstOrNull()
OnLinkClickHandler.openLink(requireContext(), preferences, Uri.parse(url))
}
override fun scrollToStart(): Boolean { override fun scrollToStart(): Boolean {
val result = super.scrollToStart() val result = super.scrollToStart()
if (result) { if (result) {

View File

@ -2,12 +2,11 @@ package org.mariotaku.twidere.task
import android.content.Context import android.content.Context
import androidx.collection.LruCache import androidx.collection.LruCache
import org.attoparser.config.ParseConfiguration import org.jsoup.Jsoup
import org.attoparser.dom.DOMMarkupParser import org.jsoup.nodes.Document
import org.attoparser.dom.Document
import org.mariotaku.ktextension.toString import org.mariotaku.ktextension.toString
import org.mariotaku.restfu.http.HttpRequest import org.mariotaku.restfu.http.HttpRequest
import org.mariotaku.twidere.extension.atto.firstElementOrNull import org.mariotaku.twidere.model.event.StatusListChangedEvent
import org.mariotaku.twidere.view.LinkPreviewData import org.mariotaku.twidere.view.LinkPreviewData
class LinkPreviewTask( class LinkPreviewTask(
@ -19,19 +18,19 @@ class LinkPreviewTask(
return null return null
} }
loadingList.add(url) loadingList.add(url)
val response = restHttpClient.newCall( val response = runCatching {
restHttpClient.newCall(
HttpRequest HttpRequest
.Builder() .Builder()
.url(url.replace("http:", "https:")) .url(url.replace("http:", "https:"))
.method("GET") .method("GET")
.build() .build()
).execute() ).execute()
//TODO: exception handling }.getOrNull()
return response.body.stream().toString(charset = Charsets.UTF_8, close = true).let { return response?.body?.stream()?.toString(charset = Charsets.UTF_8, close = true)?.let {
val parser = DOMMarkupParser(ParseConfiguration.htmlConfiguration()) Jsoup.parse(it)
parser.parse(it)
}?.let { doc -> }?.let { doc ->
val title = doc.getMeta("og:title") val title = doc.getMeta("og:title") ?: doc.title()
val desc = doc.getMeta("og:description") val desc = doc.getMeta("og:description")
val img = doc.getMeta("og:image") val img = doc.getMeta("og:image")
LinkPreviewData( LinkPreviewData(
@ -40,16 +39,15 @@ class LinkPreviewTask(
}?.also { }?.also {
cacheData.put(url, it) cacheData.put(url, it)
loadingList.remove(url) loadingList.remove(url)
//TODO: send the result back to bus
} }
} }
override fun afterExecute(callback: Any?, result: LinkPreviewData?) {
bus.post(StatusListChangedEvent())
}
private fun Document.getMeta(name: String): String? { private fun Document.getMeta(name: String): String? {
return firstElementOrNull { return this.head().getElementsByAttributeValue("property", name).firstOrNull { it.tagName() == "meta" }?.attributes()?.get("content")
it.elementNameMatches("meta") &&
it.hasAttribute("property") &&
it.getAttributeValue("property") == name
}?.getAttributeValue("content")
} }
companion object { companion object {

View File

@ -1,42 +1,49 @@
package org.mariotaku.twidere.view package org.mariotaku.twidere.view
import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.os.Build import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.FrameLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.RequestManager
import com.google.android.material.card.MaterialCardView
import kotlinx.android.synthetic.main.layout_link_preview.view.* import kotlinx.android.synthetic.main.layout_link_preview.view.*
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
data class LinkPreviewData( data class LinkPreviewData(
val title: String?, val title: String?,
val desc: String?, val desc: String? = null,
val img: String? val img: String? = null,
val imgRes: Int? = null
) )
class LinkPreviewView : FrameLayout { class LinkPreviewView : MaterialCardView {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
init { init {
LayoutInflater.from(context).inflate(R.layout.layout_link_preview, this) LayoutInflater.from(context).inflate(R.layout.layout_link_preview, this, true)
} }
fun displayData(value: String, result: LinkPreviewData) { fun displayData(value: String, result: LinkPreviewData, requestManager: RequestManager) {
link_preview_title.isVisible = true link_preview_title.isVisible = true
link_preview_link.isVisible = true link_preview_link.isVisible = true
link_preview_img.isVisible = result.img != null
link_preview_loader.isVisible = false link_preview_loader.isVisible = false
link_preview_title.text = result.title link_preview_title.text = result.title
link_preview_link.text = value link_preview_link.text = Uri.parse(value).host
if (result.img != null) {
requestManager.load(result.img).into(link_preview_img)
} else if (result.imgRes != null) {
requestManager.load(result.imgRes).into(link_preview_img)
}
} }
fun reset() { fun reset() {
link_preview_img.isVisible = false
link_preview_title.isVisible = false link_preview_title.isVisible = false
link_preview_link.isVisible = false link_preview_link.isVisible = false
link_preview_loader.isVisible = true link_preview_loader.isVisible = true

View File

@ -47,6 +47,7 @@ import org.mariotaku.twidere.util.ThemeUtils
import org.mariotaku.twidere.util.UnitConvertUtils import org.mariotaku.twidere.util.UnitConvertUtils
import org.mariotaku.twidere.util.Utils import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.util.Utils.getUserTypeIconRes import org.mariotaku.twidere.util.Utils.getUserTypeIconRes
import org.mariotaku.twidere.view.LinkPreviewData
import org.mariotaku.twidere.view.ShapedImageView import org.mariotaku.twidere.view.ShapedImageView
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -143,6 +144,8 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
quotedMediaPreview.visibility = View.GONE quotedMediaPreview.visibility = View.GONE
quotedMediaLabel.visibility = View.GONE quotedMediaLabel.visibility = View.GONE
mediaPreview.displayMedia(R.drawable.featured_graphics) mediaPreview.displayMedia(R.drawable.featured_graphics)
linkPreview.isVisible = isLinkPreviewShown
linkPreview.displayData(TWIDERE_PREVIEW_LINK_URI, LinkPreviewData(title = TWIDERE_PREVIEW_NAME, imgRes = R.drawable.featured_graphics), adapter.requestManager)
} }
override fun display(status: ParcelableStatus, displayInReplyTo: Boolean, override fun display(status: ParcelableStatus, displayInReplyTo: Boolean,
@ -349,13 +352,13 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
mediaPreview.visibility = View.GONE mediaPreview.visibility = View.GONE
} }
val url = status.extras?.external_url val url = status.extras?.entities_url?.firstOrNull()
linkPreview.isVisible = url != null linkPreview.isVisible = url != null && isLinkPreviewShown
if (url != null) { if (url != null && linkPreview.isVisible) {
if (!LinkPreviewTask.isInLoading(url)) { if (!LinkPreviewTask.isInLoading(url)) {
val linkPreviewData = LinkPreviewTask.getCached(url) val linkPreviewData = LinkPreviewTask.getCached(url)
if (linkPreviewData != null) { if (linkPreviewData != null) {
linkPreview.displayData(url, linkPreviewData) linkPreview.displayData(url, linkPreviewData, requestManager)
} else { } else {
LinkPreviewTask(context).let { LinkPreviewTask(context).let {
it.params = url it.params = url
@ -510,6 +513,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
itemContent.setOnClickListener(eventListener) itemContent.setOnClickListener(eventListener)
itemContent.setOnLongClickListener(eventListener) itemContent.setOnLongClickListener(eventListener)
linkPreview.setOnClickListener(eventListener)
itemMenu.setOnClickListener(eventListener) itemMenu.setOnClickListener(eventListener)
profileImageView.setOnClickListener(eventListener) profileImageView.setOnClickListener(eventListener)
replyButton.setOnClickListener(eventListener) replyButton.setOnClickListener(eventListener)
@ -601,6 +605,8 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
listener.onLiked() listener.onLiked()
} }
} }
private val isLinkPreviewShown: Boolean
get() = adapter.isLinkPreviewShown(layoutPosition)
private val isCardNumbersShown: Boolean private val isCardNumbersShown: Boolean
get() = adapter.isCardNumbersShown(layoutPosition) get() = adapter.isCardNumbersShown(layoutPosition)
@ -724,6 +730,9 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
listener.onStatusClick(holder, position) listener.onStatusClick(holder, position)
} }
} }
holder.linkPreview -> {
listener.onLinkClick(holder, position)
}
} }
} }

View File

@ -65,6 +65,8 @@ interface IStatusViewHolder : CardMediaContainer.OnMediaClickListener {
fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {} fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {}
fun onFilterClick(holder: TimelineFilterHeaderViewHolder) {} fun onFilterClick(holder: TimelineFilterHeaderViewHolder) {}
fun onLinkClick(holder: IStatusViewHolder, position: Int) {}
} }
} }

View File

@ -22,6 +22,7 @@ package org.mariotaku.twidere.view.holder.status
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
@ -36,10 +37,12 @@ import androidx.annotation.UiThread
import androidx.appcompat.widget.ActionMenuView import androidx.appcompat.widget.ActionMenuView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.adapter_item_status_count_label.view.* import kotlinx.android.synthetic.main.adapter_item_status_count_label.view.*
import kotlinx.android.synthetic.main.header_status.view.* import kotlinx.android.synthetic.main.header_status.view.*
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.applyFontFamily import org.mariotaku.ktextension.applyFontFamily
import org.mariotaku.ktextension.hideIfEmpty import org.mariotaku.ktextension.hideIfEmpty
@ -54,6 +57,7 @@ import org.mariotaku.twidere.annotation.ProfileImageSize
import org.mariotaku.twidere.constant.displaySensitiveContentsKey import org.mariotaku.twidere.constant.displaySensitiveContentsKey
import org.mariotaku.twidere.constant.hideCardNumbersKey import org.mariotaku.twidere.constant.hideCardNumbersKey
import org.mariotaku.twidere.constant.newDocumentApiKey import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.constant.showLinkPreviewKey
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
import org.mariotaku.twidere.extension.model.* import org.mariotaku.twidere.extension.model.*
import org.mariotaku.twidere.fragment.AbsStatusesFragment import org.mariotaku.twidere.fragment.AbsStatusesFragment
@ -63,6 +67,7 @@ import org.mariotaku.twidere.menu.RetweetItemProvider
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.util.ParcelableLocationUtils import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.model.util.ParcelableMediaUtils import org.mariotaku.twidere.model.util.ParcelableMediaUtils
import org.mariotaku.twidere.task.LinkPreviewTask
import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.twitter.card.TwitterCardViewFactory import org.mariotaku.twidere.util.twitter.card.TwitterCardViewFactory
import org.mariotaku.twidere.view.ProfileImageView import org.mariotaku.twidere.view.ProfileImageView
@ -366,6 +371,28 @@ class DetailStatusViewHolder(
textView.movementMethod = LinkMovementMethod.getInstance() textView.movementMethod = LinkMovementMethod.getInstance()
itemView.quotedText.movementMethod = null itemView.quotedText.movementMethod = null
val url = status.extras?.entities_url?.firstOrNull()
itemView.linkPreview.isVisible = url != null && fragment.preferences[showLinkPreviewKey]
if (url != null && itemView.linkPreview.isVisible) {
if (!LinkPreviewTask.isInLoading(url)) {
val linkPreviewData = LinkPreviewTask.getCached(url)
if (linkPreviewData != null) {
itemView.linkPreview.displayData(url, linkPreviewData, adapter.requestManager)
} else {
LinkPreviewTask(context).let {
it.params = url
TaskStarter.execute(it)
}
itemView.linkPreview.reset()
}
} else {
itemView.linkPreview.reset()
}
} else {
itemView.linkPreview.reset()
}
} }
override fun onClick(v: View) { override fun onClick(v: View) {
@ -373,6 +400,10 @@ class DetailStatusViewHolder(
val fragment = adapter.fragment val fragment = adapter.fragment
val preferences = fragment.preferences val preferences = fragment.preferences
when (v) { when (v) {
itemView.linkPreview -> {
val url = status.extras?.entities_url?.firstOrNull()
OnLinkClickHandler.openLink(fragment.requireContext(), preferences, Uri.parse(url))
}
itemView.mediaPreviewLoad -> { itemView.mediaPreviewLoad -> {
if (adapter.sensitiveContentEnabled || !status.is_possibly_sensitive) { if (adapter.sensitiveContentEnabled || !status.is_possibly_sensitive) {
adapter.isDetailMediaExpanded = true adapter.isDetailMediaExpanded = true
@ -472,6 +503,7 @@ class DetailStatusViewHolder(
ThemeUtils.wrapMenuIcon(itemView.menuBar, excludeGroups = *intArrayOf(Constants.MENU_GROUP_STATUS_SHARE)) ThemeUtils.wrapMenuIcon(itemView.menuBar, excludeGroups = *intArrayOf(Constants.MENU_GROUP_STATUS_SHARE))
itemView.mediaPreviewLoad.setOnClickListener(this) itemView.mediaPreviewLoad.setOnClickListener(this)
itemView.profileContainer.setOnClickListener(this) itemView.profileContainer.setOnClickListener(this)
itemView.linkPreview.setOnClickListener(this)
retweetedByView.setOnClickListener(this) retweetedByView.setOnClickListener(this)
locationView.setOnClickListener(this) locationView.setOnClickListener(this)
itemView.quotedView.setOnClickListener(this) itemView.quotedView.setOnClickListener(this)

View File

@ -290,11 +290,18 @@
</FrameLayout> </FrameLayout>
<org.mariotaku.twidere.view.LinkPreviewView
android:id="@+id/linkPreview"
android:layout_margin="@dimen/element_spacing_normal"
android:layout_below="@+id/mediaPreviewContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<org.mariotaku.twidere.view.TwitterCardContainer <org.mariotaku.twidere.view.TwitterCardContainer
android:id="@+id/twitterCard" android:id="@+id/twitterCard"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/mediaPreviewContainer" android:layout_below="@+id/linkPreview"
android:visibility="gone"/> android:visibility="gone"/>
<org.mariotaku.twidere.view.ColorLabelLinearLayout <org.mariotaku.twidere.view.ColorLabelLinearLayout

View File

@ -1,25 +1,44 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <merge
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:padding="8dp" <ImageView
xmlns:tools="http://schemas.android.com/tools"> tools:src="@mipmap/ic_launcher"
android:id="@+id/link_preview_img"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView <TextView
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text" android:paddingRight="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:paddingTop="8dp"
style="@style/TextAppearance.MaterialComponents.Body1"
tools:text="Google" tools:text="Google"
android:id="@+id/link_preview_title" android:id="@+id/link_preview_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:paddingRight="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:paddingBottom="8dp"
android:singleLine="true"
android:maxLines="1"
android:id="@+id/link_preview_link" android:id="@+id/link_preview_link"
tools:text="https://www.google.com" tools:text="https://www.google.com"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<org.mariotaku.chameleon.view.ChameleonProgressBar <org.mariotaku.chameleon.view.ChameleonProgressBar
tools:visibility="gone" android:visibility="gone"
android:id="@+id/link_preview_loader" android:id="@+id/link_preview_loader"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
</merge>

View File

@ -212,6 +212,7 @@
<org.mariotaku.twidere.view.LinkPreviewView <org.mariotaku.twidere.view.LinkPreviewView
android:id="@+id/linkPreview" android:id="@+id/linkPreview"
android:layout_marginTop="@dimen/element_spacing_normal"
android:layout_below="@+id/mediaPreview" android:layout_below="@+id/mediaPreview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
@ -220,7 +221,7 @@
android:id="@+id/quotedView" android:id="@+id/quotedView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/mediaPreview" android:layout_below="@+id/linkPreview"
android:layout_marginTop="@dimen/element_spacing_small" android:layout_marginTop="@dimen/element_spacing_small"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:clickable="true" android:clickable="true"
@ -314,6 +315,7 @@
<include layout="@layout/layout_card_media_preview"/> <include layout="@layout/layout_card_media_preview"/>
</org.mariotaku.twidere.view.CardMediaContainer> </org.mariotaku.twidere.view.CardMediaContainer>
</org.mariotaku.twidere.view.ColorLabelRelativeLayout> </org.mariotaku.twidere.view.ColorLabelRelativeLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -446,6 +446,7 @@
<string name="hide_card_actions">Hide card actions</string> <string name="hide_card_actions">Hide card actions</string>
<string name="hide_card_numbers">Hide card numbers</string> <string name="hide_card_numbers">Hide card numbers</string>
<string name="show_link_preview">Show link preview</string>
<string name="hide_quotes">Hide quotes</string> <string name="hide_quotes">Hide quotes</string>
<string name="hide_replies">Hide replies</string> <string name="hide_replies">Hide replies</string>
<string name="hide_retweets">Hide retweets</string> <string name="hide_retweets">Hide retweets</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config>
<domain-config cleartextTrafficPermitted="true"> <base-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">fanfou.com</domain> </base-config>
</domain-config>
</network-security-config> </network-security-config>

View File

@ -111,8 +111,18 @@
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="i_want_my_stars_back" android:key="show_link_preview"
android:order="35" android:order="35"
android:title="@string/show_link_preview">
<extra
android:name="should_recreate"
android:value="true"/>
</SwitchPreferenceCompat>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="i_want_my_stars_back"
android:order="36"
android:summary="@string/i_want_my_stars_back_summary" android:summary="@string/i_want_my_stars_back_summary"
android:title="@string/i_want_my_stars_back"> android:title="@string/i_want_my_stars_back">
<extra <extra
@ -123,7 +133,7 @@
<org.mariotaku.twidere.preference.FavoriteConfirmSwitchPreference <org.mariotaku.twidere.preference.FavoriteConfirmSwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="favorite_confirmation" android:key="favorite_confirmation"
android:order="36" android:order="37"
android:summary="@string/preference_summary_favorite_confirmation" android:summary="@string/preference_summary_favorite_confirmation"
android:title="@string/preference_title_favorite_confirmation"/> android:title="@string/preference_title_favorite_confirmation"/>