mirror of
https://github.com/pachli/pachli-android.git
synced 2025-02-03 10:47:34 +01:00
feat: Show posts that mention a trending link (#1153)
Mastodon 4.3 introduced a new API to fetch a timeline of posts that mention a trending link. Use that to display a "See <n> posts about ths link" message in a trending link's preview card (if supported by the server). Define a new timeline type with associated API call to fetch the timeline. Add an accessibilty action to support this. While I'm here also support author's in preview cards that don't have a related Fediverse account; show their name in this case. Fixes #1123
This commit is contained in:
parent
cfab7a9dfe
commit
57be148fbf
@ -161,6 +161,7 @@ data class TabViewData(
|
||||
icon = R.drawable.ic_favourite_filled_24dp,
|
||||
fragment = { TimelineFragment.newInstance(pachliAccountId, timeline) },
|
||||
)
|
||||
is Timeline.Link -> throw IllegalArgumentException("can't add to tab: $timeline")
|
||||
is Timeline.User.Pinned -> throw IllegalArgumentException("can't add to tab: $timeline")
|
||||
is Timeline.User.Posts -> throw IllegalArgumentException("can't add to tab: $timeline")
|
||||
is Timeline.User.Replies -> throw IllegalArgumentException("can't add to tab: $timeline")
|
||||
|
@ -895,7 +895,7 @@ abstract class StatusBaseViewHolder<T : IStatusViewData> protected constructor(
|
||||
(!viewData.isCollapsible || !viewData.isCollapsed)
|
||||
) {
|
||||
cardView.visibility = View.VISIBLE
|
||||
cardView.bind(card, viewData.actionable.sensitive, statusDisplayOptions) { card, target ->
|
||||
cardView.bind(card, viewData.actionable.sensitive, statusDisplayOptions, false) { card, target ->
|
||||
if (target == PreviewCardView.Target.BYLINE) {
|
||||
card.authors?.firstOrNull()?.account?.id?.let {
|
||||
context.startActivity(AccountActivityIntent(context, pachliAccountId, it))
|
||||
|
@ -772,6 +772,7 @@ class TimelineFragment :
|
||||
Timeline.Notifications,
|
||||
Timeline.TrendingHashtags,
|
||||
Timeline.TrendingLinks,
|
||||
is Timeline.Link,
|
||||
-> return
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,7 @@ class NetworkTimelineRemoteMediator(
|
||||
Timeline.PublicFederated -> api.publicTimeline(local = false, maxId = maxId, minId = minId, limit = loadSize)
|
||||
Timeline.PublicLocal -> api.publicTimeline(local = true, maxId = maxId, minId = minId, limit = loadSize)
|
||||
Timeline.TrendingStatuses -> api.trendingStatuses(limit = LIMIT_TRENDING_STATUSES)
|
||||
is Timeline.Link -> api.linkTimeline(url = timeline.url, maxId = maxId, minId = minId, limit = loadSize)
|
||||
is Timeline.Hashtags -> {
|
||||
val firstHashtag = timeline.tags.first()
|
||||
val additionalHashtags = timeline.tags.subList(1, timeline.tags.size)
|
||||
|
@ -29,9 +29,15 @@ class TrendingLinkViewHolder(
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
internal lateinit var link: TrendsLink
|
||||
|
||||
fun bind(link: TrendsLink, statusDisplayOptions: StatusDisplayOptions) {
|
||||
/**
|
||||
* @param link
|
||||
* @param statusDisplayOptions
|
||||
* @param showTimelineLink True if the UI to view a timeline of statuses about this link
|
||||
* should be shown.
|
||||
*/
|
||||
fun bind(link: TrendsLink, statusDisplayOptions: StatusDisplayOptions, showTimelineLink: Boolean) {
|
||||
this.link = link
|
||||
|
||||
binding.statusCardView.bind(link, sensitive = false, statusDisplayOptions, onClick)
|
||||
binding.statusCardView.bind(link, sensitive = false, statusDisplayOptions, showTimelineLink, onClick)
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,9 @@ import dagger.hilt.android.EntryPointAccessors
|
||||
* Each item shows an action to open the link.
|
||||
*
|
||||
* If present, an item to show the author's account is also included.
|
||||
*
|
||||
* If supported, an item to show a timeline of statuses that mention this link
|
||||
* is included.
|
||||
*/
|
||||
internal class TrendingLinksAccessibilityDelegate(
|
||||
private val recyclerView: RecyclerView,
|
||||
@ -59,6 +62,11 @@ internal class TrendingLinksAccessibilityDelegate(
|
||||
context.getString(R.string.action_open_byline_account),
|
||||
)
|
||||
|
||||
private val openTimelineLinkAction = AccessibilityActionCompat(
|
||||
app.pachli.core.ui.R.id.action_timeline_link,
|
||||
context.getString(R.string.action_timeline_link),
|
||||
)
|
||||
|
||||
private val delegate = object : ItemDelegate(this) {
|
||||
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info)
|
||||
@ -72,6 +80,10 @@ internal class TrendingLinksAccessibilityDelegate(
|
||||
viewHolder.link.authors?.firstOrNull()?.account?.let {
|
||||
info.addAction(openBylineAccountAction)
|
||||
}
|
||||
|
||||
if ((recyclerView.adapter as? TrendingLinksAdapter)?.showTimelineLink == true) {
|
||||
info.addAction(openTimelineLinkAction)
|
||||
}
|
||||
}
|
||||
|
||||
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
|
||||
@ -93,6 +105,11 @@ internal class TrendingLinksAccessibilityDelegate(
|
||||
listener.onClick(viewHolder.link, Target.BYLINE)
|
||||
true
|
||||
}
|
||||
app.pachli.core.ui.R.id.action_timeline_link -> {
|
||||
interrupt()
|
||||
listener.onClick(viewHolder.link, Target.TIMELINE_LINK)
|
||||
true
|
||||
}
|
||||
else -> super.performAccessibilityAction(host, action, args)
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,15 @@ import app.pachli.core.network.model.TrendsLink
|
||||
import app.pachli.databinding.ItemTrendingLinkBinding
|
||||
import app.pachli.view.PreviewCardView
|
||||
|
||||
/**
|
||||
* @param statusDisplayOptions
|
||||
* @param showTimelineLink If true, show a link to a timeline with statuses that
|
||||
* mention this link.
|
||||
* @param onViewLink
|
||||
*/
|
||||
class TrendingLinksAdapter(
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
showTimelineLink: Boolean,
|
||||
private val onViewLink: PreviewCardView.OnClickListener,
|
||||
) : ListAdapter<TrendsLink, TrendingLinkViewHolder>(diffCallback) {
|
||||
var statusDisplayOptions = statusDisplayOptions
|
||||
@ -37,6 +44,14 @@ class TrendingLinksAdapter(
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
var showTimelineLink = showTimelineLink
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
||||
}
|
||||
@ -49,7 +64,7 @@ class TrendingLinksAdapter(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrendingLinkViewHolder, position: Int) {
|
||||
holder.bind(getItem(position), statusDisplayOptions)
|
||||
holder.bind(getItem(position), statusDisplayOptions, showTimelineLink)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
|
@ -45,7 +45,9 @@ import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.model.ServerOperation
|
||||
import app.pachli.core.navigation.AccountActivityIntent
|
||||
import app.pachli.core.navigation.TimelineActivityIntent
|
||||
import app.pachli.core.network.model.PreviewCard
|
||||
import app.pachli.core.ui.ActionButtonScrollListener
|
||||
import app.pachli.core.ui.BackgroundMessage
|
||||
@ -60,8 +62,11 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||
import io.github.z4kn4fein.semver.constraints.toConstraint
|
||||
import kotlin.properties.Delegates
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
@ -74,7 +79,13 @@ class TrendingLinksFragment :
|
||||
RefreshableFragment,
|
||||
MenuProvider {
|
||||
|
||||
private val viewModel: TrendingLinksViewModel by viewModels()
|
||||
private val viewModel: TrendingLinksViewModel by viewModels(
|
||||
extrasProducer = {
|
||||
defaultViewModelCreationExtras.withCreationCallback<TrendingLinksViewModel.Factory> { factory ->
|
||||
factory.create(requireArguments().getLong(ARG_PACHLI_ACCOUNT_ID))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val binding by viewBinding(FragmentTrendingLinksBinding::bind)
|
||||
|
||||
@ -99,11 +110,6 @@ class TrendingLinksFragment :
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
trendingLinksAdapter = TrendingLinksAdapter(viewModel.statusDisplayOptions.value, ::onOpenLink)
|
||||
|
||||
setupSwipeRefreshLayout()
|
||||
setupRecyclerView()
|
||||
|
||||
(activity as? ActionButtonActivity)?.actionButton?.let { actionButton ->
|
||||
actionButton.show()
|
||||
|
||||
@ -119,14 +125,47 @@ class TrendingLinksFragment :
|
||||
}
|
||||
}
|
||||
|
||||
trendingLinksAdapter = TrendingLinksAdapter(
|
||||
viewModel.statusDisplayOptions.value,
|
||||
false,
|
||||
::onOpenLink,
|
||||
)
|
||||
|
||||
setupSwipeRefreshLayout()
|
||||
setupRecyclerView()
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.loadState.collectLatest {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
viewModel.pachliAccountFlow.distinctUntilChangedBy { it.server }.collect { account ->
|
||||
trendingLinksAdapter.showTimelineLink = account.server.can(
|
||||
ServerOperation.ORG_JOINMASTODON_TIMELINES_LINK,
|
||||
">=1.0.0".toConstraint(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.loadState.collect {
|
||||
when (it) {
|
||||
LoadState.Initial -> {
|
||||
LoadState.Loading -> bindLoading()
|
||||
is LoadState.Success -> bindSuccess(it)
|
||||
is LoadState.Error -> bindError(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
viewModel.statusDisplayOptions.collectLatest {
|
||||
trendingLinksAdapter.statusDisplayOptions = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.accept(InfallibleUiAction.Reload)
|
||||
}
|
||||
|
||||
LoadState.Loading -> {
|
||||
private fun bindLoading() {
|
||||
if (!binding.swipeRefreshLayout.isRefreshing) {
|
||||
binding.progressBar.show()
|
||||
} else {
|
||||
@ -134,11 +173,11 @@ class TrendingLinksFragment :
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.Success -> {
|
||||
trendingLinksAdapter.submitList(it.data)
|
||||
private fun bindSuccess(loadState: LoadState.Success) {
|
||||
trendingLinksAdapter.submitList(loadState.data)
|
||||
binding.progressBar.hide()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
if (it.data.isEmpty()) {
|
||||
if (loadState.data.isEmpty()) {
|
||||
binding.messageView.setup(BackgroundMessage.Empty())
|
||||
binding.messageView.show()
|
||||
} else {
|
||||
@ -147,42 +186,32 @@ class TrendingLinksFragment :
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.Error -> {
|
||||
private fun bindError(loadState: LoadState.Error) {
|
||||
binding.progressBar.hide()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
binding.recyclerView.hide()
|
||||
if (trendingLinksAdapter.itemCount != 0) {
|
||||
val snackbar = Snackbar.make(
|
||||
binding.root,
|
||||
it.throwable.message ?: "Error",
|
||||
loadState.throwable.message ?: "Error",
|
||||
Snackbar.LENGTH_INDEFINITE,
|
||||
)
|
||||
|
||||
if (it.throwable !is HttpException || it.throwable.code() != 404) {
|
||||
if (loadState.throwable !is HttpException || loadState.throwable.code() != 404) {
|
||||
snackbar.setAction("Retry") { viewModel.accept(InfallibleUiAction.Reload) }
|
||||
}
|
||||
snackbar.show()
|
||||
} else {
|
||||
if (it.throwable !is HttpException || it.throwable.code() != 404) {
|
||||
binding.messageView.setup(it.throwable) {
|
||||
if (loadState.throwable !is HttpException || loadState.throwable.code() != 404) {
|
||||
binding.messageView.setup(loadState.throwable) {
|
||||
viewModel.accept(InfallibleUiAction.Reload)
|
||||
}
|
||||
} else {
|
||||
binding.messageView.setup(it.throwable)
|
||||
binding.messageView.setup(loadState.throwable)
|
||||
}
|
||||
binding.messageView.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.statusDisplayOptions.collectLatest {
|
||||
trendingLinksAdapter.statusDisplayOptions = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSwipeRefreshLayout() {
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
||||
@ -238,14 +267,22 @@ class TrendingLinksFragment :
|
||||
}
|
||||
|
||||
private fun onOpenLink(card: PreviewCard, target: Target) {
|
||||
if (target == Target.BYLINE) {
|
||||
card.authors?.firstOrNull()?.account?.id?.let {
|
||||
when (target) {
|
||||
Target.CARD -> requireContext().openLink(card.url)
|
||||
Target.IMAGE -> requireContext().openLink(card.url)
|
||||
Target.BYLINE -> card.authors?.firstOrNull()?.account?.id?.let {
|
||||
startActivity(AccountActivityIntent(requireContext(), pachliAccountId, it))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
requireContext().openLink(card.url)
|
||||
Target.TIMELINE_LINK -> {
|
||||
val intent = TimelineActivityIntent.link(
|
||||
requireContext(),
|
||||
pachliAccountId,
|
||||
card.url,
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -19,7 +19,9 @@ package app.pachli.components.trending.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.components.account.AccountViewModel
|
||||
import app.pachli.components.trending.TrendingLinksRepository
|
||||
import app.pachli.core.common.extensions.stateFlow
|
||||
import app.pachli.core.common.extensions.throttleFirst
|
||||
import app.pachli.core.data.repository.AccountManager
|
||||
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
|
||||
@ -27,20 +29,22 @@ import app.pachli.core.network.model.TrendsLink
|
||||
import app.pachli.core.preferences.PrefKeys
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed interface UiAction
|
||||
@ -50,23 +54,38 @@ sealed interface InfallibleUiAction : UiAction {
|
||||
}
|
||||
|
||||
sealed interface LoadState {
|
||||
data object Initial : LoadState
|
||||
data object Loading : LoadState
|
||||
data class Success(val data: List<TrendsLink>) : LoadState
|
||||
data class Error(val throwable: Throwable) : LoadState
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class TrendingLinksViewModel @Inject constructor(
|
||||
@HiltViewModel(assistedFactory = TrendingLinksViewModel.Factory::class)
|
||||
class TrendingLinksViewModel @AssistedInject constructor(
|
||||
@Assisted private val pachliAccountId: Long,
|
||||
private val repository: TrendingLinksRepository,
|
||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||
accountManager: AccountManager,
|
||||
) : ViewModel() {
|
||||
val activeAccount = accountManager.activeAccount!!
|
||||
val pachliAccountFlow = accountManager.getPachliAccountFlow(pachliAccountId)
|
||||
.filterNotNull()
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
||||
|
||||
private val _loadState = MutableStateFlow<LoadState>(LoadState.Initial)
|
||||
val loadState = _loadState.asStateFlow()
|
||||
private val reload = MutableSharedFlow<Unit>(replay = 1)
|
||||
|
||||
val loadState = stateFlow(viewModelScope, LoadState.Loading) {
|
||||
reload.flatMapLatest {
|
||||
flow {
|
||||
emit(LoadState.Loading)
|
||||
emit(
|
||||
repository.getTrendingLinks().fold(
|
||||
{ list -> LoadState.Success(list) },
|
||||
{ throwable -> LoadState.Error(throwable) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}.flowWhileShared(SharingStarted.WhileSubscribed(5000))
|
||||
}
|
||||
|
||||
val showFabWhileScrolling = sharedPreferencesRepository.changes
|
||||
.filter { it == null || it == PrefKeys.FAB_HIDE }
|
||||
@ -85,17 +104,14 @@ class TrendingLinksViewModel @Inject constructor(
|
||||
uiAction
|
||||
.throttleFirst()
|
||||
.filterIsInstance<InfallibleUiAction.Reload>()
|
||||
.onEach { invalidate() }
|
||||
.onEach { reload.emit(Unit) }
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidate() = viewModelScope.launch {
|
||||
_loadState.update { LoadState.Loading }
|
||||
val response = repository.getTrendingLinks()
|
||||
response.fold(
|
||||
{ list -> _loadState.update { LoadState.Success(list) } },
|
||||
{ throwable -> _loadState.update { LoadState.Error(throwable) } },
|
||||
)
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
/** Creates [AccountViewModel] with [pachliAccountId] as the active account. */
|
||||
fun create(pachliAccountId: Long): TrendingLinksViewModel
|
||||
}
|
||||
}
|
||||
|
@ -21,19 +21,21 @@ import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.text.HtmlCompat
|
||||
import app.pachli.R
|
||||
import app.pachli.core.activity.decodeBlurHash
|
||||
import app.pachli.core.activity.emojify
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.string.unicodeWrap
|
||||
import app.pachli.core.common.util.formatNumber
|
||||
import app.pachli.core.data.model.StatusDisplayOptions
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.network.model.PreviewCard
|
||||
import app.pachli.core.network.model.TrendsLink
|
||||
import app.pachli.databinding.PreviewCardBinding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.MultiTransformation
|
||||
@ -66,6 +68,9 @@ class PreviewCardView @JvmOverloads constructor(
|
||||
|
||||
/** The author byline */
|
||||
BYLINE,
|
||||
|
||||
/** The link timeline */
|
||||
TIMELINE_LINK,
|
||||
}
|
||||
|
||||
fun interface OnClickListener {
|
||||
@ -114,12 +119,15 @@ class PreviewCardView @JvmOverloads constructor(
|
||||
* @param card The card to bind
|
||||
* @param sensitive True if the status that contained this card was marked sensitive
|
||||
* @param statusDisplayOptions
|
||||
* @param showTimelineLink True if the UI to view a timeline of statuses about this link
|
||||
* should be shown.
|
||||
* @param listener
|
||||
*/
|
||||
fun bind(
|
||||
card: PreviewCard,
|
||||
sensitive: Boolean,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
showTimelineLink: Boolean,
|
||||
listener: OnClickListener,
|
||||
): Unit = with(binding) {
|
||||
cardTitle.text = card.title
|
||||
@ -135,9 +143,8 @@ class PreviewCardView @JvmOverloads constructor(
|
||||
|
||||
previewCardWrapper.setOnClickListener { listener.onClick(card, Target.CARD) }
|
||||
cardImage.setOnClickListener { listener.onClick(card, Target.IMAGE) }
|
||||
byline.referencedIds.forEach { id ->
|
||||
root.findViewById<View>(id).setOnClickListener { listener.onClick(card, Target.BYLINE) }
|
||||
}
|
||||
authorInfo.setOnClickListener { listener.onClick(card, Target.BYLINE) }
|
||||
timelineLink.setOnClickListener { listener.onClick(card, Target.TIMELINE_LINK) }
|
||||
|
||||
cardLink.text = card.url
|
||||
|
||||
@ -177,14 +184,60 @@ class PreviewCardView @JvmOverloads constructor(
|
||||
cardImage.hide()
|
||||
}
|
||||
|
||||
card.authors?.firstOrNull()?.account?.let { account ->
|
||||
val name = account.name.unicodeWrap().emojify(account.emojis, authorInfo, false)
|
||||
authorInfo.text = authorInfo.context.getString(R.string.preview_card_byline_fmt, name)
|
||||
var showBylineDivider = false
|
||||
bylineDivider.hide()
|
||||
|
||||
Glide.with(authorInfo.context).load(account.avatar).transform(bylineAvatarTransformation)
|
||||
// Determine how to show the author info (if present)
|
||||
val author = card.authors?.firstOrNull()
|
||||
when {
|
||||
// Author has an account, link to that, with their avatar.
|
||||
author?.account != null -> {
|
||||
val name = author.account?.name.unicodeWrap().emojify(author.account?.emojis, authorInfo, false)
|
||||
authorInfo.text = HtmlCompat.fromHtml(
|
||||
authorInfo.context.getString(R.string.preview_card_byline_fediverse_account_fmt, name),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
)
|
||||
|
||||
Glide.with(authorInfo.context).load(author.account?.avatar).transform(bylineAvatarTransformation)
|
||||
.placeholder(DR.drawable.avatar_default).into(bylineAvatarTarget)
|
||||
byline.show()
|
||||
} ?: byline.hide()
|
||||
authorInfo.show()
|
||||
showBylineDivider = true
|
||||
}
|
||||
|
||||
// Author has a name but no account. Show the name, clear the avatar.
|
||||
// It's not enough that the name is present, it can't be empty, because of
|
||||
// https://github.com/mastodon/mastodon/issues/33139).
|
||||
!author?.name.isNullOrBlank() -> {
|
||||
authorInfo.text = HtmlCompat.fromHtml(
|
||||
authorInfo.context.getString(R.string.preview_card_byline_name_only_fmt, author?.name),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
)
|
||||
authorInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
|
||||
authorInfo.show()
|
||||
showBylineDivider = true
|
||||
}
|
||||
|
||||
else -> authorInfo.hide()
|
||||
}
|
||||
|
||||
// TrendsLink cards have data about the usage. Show this if the server
|
||||
// can generate the timeline.
|
||||
if (card is TrendsLink && showTimelineLink) {
|
||||
val count = card.history.sumOf { it.uses }
|
||||
timelineLink.text = HtmlCompat.fromHtml(
|
||||
context.getString(
|
||||
R.string.preview_card_timeline_link_fmt,
|
||||
formatNumber(count.toLong()),
|
||||
),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
)
|
||||
timelineLink.show()
|
||||
showBylineDivider = true
|
||||
} else {
|
||||
timelineLink.hide()
|
||||
}
|
||||
|
||||
if (showBylineDivider) bylineDivider.show()
|
||||
}
|
||||
|
||||
/** Adjusts the layout parameters to place the image above the information views */
|
||||
|
@ -126,12 +126,25 @@
|
||||
tools:ignore="SelectableText"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/byline"
|
||||
android:layout_width="wrap_content"
|
||||
<TextView
|
||||
android:id="@+id/timeline_link"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="byline_divider,author_info" />
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="10dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start|center"
|
||||
android:lines="1"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/author_info"
|
||||
tools:ignore="SelectableText"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</merge>
|
||||
|
@ -638,7 +638,7 @@
|
||||
<string name="error_media_uploader_upload_not_found_fmt">no se encontró la subida de medios con ID de %1$s</string>
|
||||
<string name="error_prepare_media_unknown_file_size">no se pudo determinar el tamaño del archivo</string>
|
||||
<string name="error_prepare_media_unknown_mime_type">se desconoce el tipo de archivo</string>
|
||||
<string name="preview_card_byline_fmt">Ver más de %1$s</string>
|
||||
<string name="preview_card_byline_fediverse_account_fmt">Ver más de %1$s</string>
|
||||
<string name="action_open_byline_account">Mostrar el perfil del autor del artículo</string>
|
||||
<string name="action_open_link">Abrir enlace</string>
|
||||
<string name="compose_warn_language_dialog_title">Comprueba el idioma de la publicación</string>
|
||||
|
@ -628,7 +628,7 @@
|
||||
<string name="error_prepare_media_content_resolver_unsupported_scheme_fmt">sisältöselvittäjä-URI:n kaavaa ei tueta: %1$s</string>
|
||||
<string name="error_prepare_media_unknown_mime_type">tuntematon tiedostotyyppi</string>
|
||||
<string name="error_pick_media_fmt">Julkaisuun ei voitu liittää tiedostoa: %1$s</string>
|
||||
<string name="preview_card_byline_fmt">Katso lisää täältä: %1$s</string>
|
||||
<string name="preview_card_byline_fediverse_account_fmt">Katso lisää täältä: %1$s</string>
|
||||
<string name="action_open_byline_account">Näytä julkaisun tekijän profiili</string>
|
||||
<string name="action_open_link">Avaa linkki</string>
|
||||
<string name="error_prepare_media_unknown_file_size">tiedoston kokoa ei voitu määrittää</string>
|
||||
|
@ -692,7 +692,7 @@
|
||||
<string name="pref_title_notification_method">Modh fógartha</string>
|
||||
<string name="pref_notification_method_all_pull">Fetched thart ar uair amháin gach 15 nóiméad. Tapáil le haghaidh sonraí.</string>
|
||||
<string name="pref_title_notification_battery_optimisation">Leas iomlán a bhaint ceallraí</string>
|
||||
<string name="preview_card_byline_fmt">Féach níos mó ó %1$s</string>
|
||||
<string name="preview_card_byline_fediverse_account_fmt">Féach níos mó ó %1$s</string>
|
||||
<string name="action_open_byline_account">Taispeáin próifíl údar an ailt</string>
|
||||
<string name="action_open_link">Oscail nasc</string>
|
||||
<string name="search_operator_attachment_dialog_title">Teorainn le poist leis na meáin?</string>
|
||||
|
@ -626,7 +626,7 @@
|
||||
<string name="error_prepare_media_unknown_mime_type">tipo de ficheiro descoñecido</string>
|
||||
<string name="error_prepare_media_unsupported_mime_type_fmt">o servidor non é compatible co tipo de ficheiro: %1$s</string>
|
||||
<string name="error_pick_media_fmt">Non se anexou o ficheiro á publicación: %1$s</string>
|
||||
<string name="preview_card_byline_fmt">Le máis de %1$s</string>
|
||||
<string name="preview_card_byline_fediverse_account_fmt">Le máis de %1$s</string>
|
||||
<string name="error_load_filter_failed_fmt">Fallou a carga do filtro: %1$s</string>
|
||||
<string name="error_save_filter_failed_fmt">Non se gardou o filtro: %1$s</string>
|
||||
<string name="search_operator_attachment_dialog_image_label">Imaxes</string>
|
||||
|
@ -735,7 +735,7 @@
|
||||
<string name="search_operator_attachment_no_media_label">Ingen media</string>
|
||||
<string name="error_prepare_media_unknown_mime_type">filtypen er ukjent</string>
|
||||
<string name="error_pick_media_fmt">Kunne ikke legge filen ved innlegget: %1$s</string>
|
||||
<string name="preview_card_byline_fmt">Se mer fra %1$s</string>
|
||||
<string name="preview_card_byline_fediverse_account_fmt">Se mer fra %1$s</string>
|
||||
<string name="action_open_byline_account">Vis artikkelforfatterens profil</string>
|
||||
<string name="action_open_link">Åpne lenk</string>
|
||||
<string name="error_load_filter_failed_fmt">Innlasting av filter mislyktes: %1$s</string>
|
||||
|
@ -733,10 +733,14 @@
|
||||
<string name="error_prepare_media_unknown_mime_type">file\'s type is not known</string>
|
||||
|
||||
<string name="error_pick_media_fmt">Could not attach file to post: %1$s</string>
|
||||
<string name="preview_card_byline_fmt">See more from %1$s</string>
|
||||
<string name="preview_card_byline_fediverse_account_fmt">By <b>%1$s</b>. See more posts</string>
|
||||
<string name="preview_card_byline_name_only_fmt">By <b>%1$s</b></string>
|
||||
<string name="action_open_byline_account">Show article author\'s profile</string>
|
||||
<string name="action_open_link">Open link</string>
|
||||
|
||||
<string name="preview_card_timeline_link_fmt">See <b>%1$s</b> posts about this link</string>
|
||||
<string name="action_timeline_link">Posts about this link</string>
|
||||
|
||||
<string name="error_load_filter_failed_fmt">Loading filter failed: %1$s</string>
|
||||
<string name="error_save_filter_failed_fmt">Saving filter failed: %1$s</string>
|
||||
<string name="error_delete_filter_failed_fmt">Deleting filter failed: %1$s</string>
|
||||
|
@ -58,6 +58,7 @@ import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SE
|
||||
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE
|
||||
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED
|
||||
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLATE
|
||||
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_TIMELINES_LINK
|
||||
import app.pachli.core.network.R
|
||||
import app.pachli.core.network.model.InstanceV1
|
||||
import app.pachli.core.network.model.InstanceV2
|
||||
@ -333,6 +334,11 @@ data class Server(
|
||||
c[ORG_JOINMASTODON_SEARCH_QUERY_FROM] = "1.0.0".toVersion()
|
||||
}
|
||||
}
|
||||
|
||||
// Link timelines
|
||||
when {
|
||||
v >= "4.3.0".toVersion() -> c[ORG_JOINMASTODON_TIMELINES_LINK] = "1.0.0".toVersion()
|
||||
}
|
||||
}
|
||||
|
||||
GOTOSOCIAL -> {
|
||||
|
@ -137,6 +137,7 @@ enum class FilterContext {
|
||||
Timeline.TrendingStatuses,
|
||||
Timeline.TrendingHashtags,
|
||||
Timeline.TrendingLinks,
|
||||
is Timeline.Link,
|
||||
-> PUBLIC
|
||||
Timeline.Conversations -> null
|
||||
}
|
||||
|
@ -111,4 +111,13 @@ enum class ServerOperation(id: String, versions: List<Version>) {
|
||||
Version(major = 1),
|
||||
),
|
||||
),
|
||||
|
||||
/** Fetch statuses that mention a specific URL. */
|
||||
ORG_JOINMASTODON_TIMELINES_LINK(
|
||||
"org.joinmastodon.timelines.link",
|
||||
listOf(
|
||||
// Initial introduction in Mastodon 4.3.0
|
||||
Version(major = 1),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
@ -112,6 +112,12 @@ sealed interface Timeline : Parcelable {
|
||||
@TypeLabel("trending_statuses")
|
||||
data object TrendingStatuses : Timeline
|
||||
|
||||
/** Timeline of statuses that mention [url]. */
|
||||
@Parcelize
|
||||
@TypeLabel("link")
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Link(val url: String) : Timeline
|
||||
|
||||
// TODO: DRAFTS
|
||||
|
||||
// TODO: SCHEDULED
|
||||
|
@ -585,6 +585,16 @@ class TimelineActivityIntent private constructor(context: Context, pachliAccount
|
||||
putExtra(EXTRA_TIMELINE, Timeline.Hashtags(listOf(hashtag)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Show statuses that reference a trending link.
|
||||
*
|
||||
* @param context
|
||||
*
|
||||
*/
|
||||
fun link(context: Context, pachliAccountId: Long, url: String) = TimelineActivityIntent(context, pachliAccountId).apply {
|
||||
putExtra(EXTRA_TIMELINE, Timeline.Link(url))
|
||||
}
|
||||
|
||||
/**
|
||||
* Show statuses from a list.
|
||||
*
|
||||
|
@ -73,6 +73,7 @@ enum class FilterContext {
|
||||
Timeline.TrendingStatuses,
|
||||
Timeline.TrendingHashtags,
|
||||
Timeline.TrendingLinks,
|
||||
is Timeline.Link,
|
||||
-> PUBLIC
|
||||
Timeline.Conversations -> null
|
||||
}
|
||||
|
@ -152,6 +152,14 @@ interface MastodonApi {
|
||||
@Query("limit") limit: Int? = null,
|
||||
): Response<List<Status>>
|
||||
|
||||
@GET("api/v1/timelines/link")
|
||||
suspend fun linkTimeline(
|
||||
@Query("url") url: String,
|
||||
@Query("max_id") maxId: String? = null,
|
||||
@Query("min_id") minId: String? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
): Response<List<Status>>
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
suspend fun notifications(
|
||||
/** Return results older than this ID */
|
||||
|
@ -46,6 +46,9 @@
|
||||
<!-- Open the link in a preview card -->
|
||||
<item name="action_open_link" type="id" />
|
||||
|
||||
<!-- Open the timeline of statuses that mention a link. -->
|
||||
<item name="action_timeline_link" type="id" />
|
||||
|
||||
<!-- Copy the item -->
|
||||
<item name="action_copy_item" type="id" />
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user