mirror of
https://github.com/pachli/pachli-android.git
synced 2025-02-09 08:28:43 +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,
|
icon = R.drawable.ic_favourite_filled_24dp,
|
||||||
fragment = { TimelineFragment.newInstance(pachliAccountId, timeline) },
|
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.Pinned -> throw IllegalArgumentException("can't add to tab: $timeline")
|
||||||
is Timeline.User.Posts -> 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")
|
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)
|
(!viewData.isCollapsible || !viewData.isCollapsed)
|
||||||
) {
|
) {
|
||||||
cardView.visibility = View.VISIBLE
|
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) {
|
if (target == PreviewCardView.Target.BYLINE) {
|
||||||
card.authors?.firstOrNull()?.account?.id?.let {
|
card.authors?.firstOrNull()?.account?.id?.let {
|
||||||
context.startActivity(AccountActivityIntent(context, pachliAccountId, it))
|
context.startActivity(AccountActivityIntent(context, pachliAccountId, it))
|
||||||
|
@ -772,6 +772,7 @@ class TimelineFragment :
|
|||||||
Timeline.Notifications,
|
Timeline.Notifications,
|
||||||
Timeline.TrendingHashtags,
|
Timeline.TrendingHashtags,
|
||||||
Timeline.TrendingLinks,
|
Timeline.TrendingLinks,
|
||||||
|
is Timeline.Link,
|
||||||
-> return
|
-> return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,7 @@ class NetworkTimelineRemoteMediator(
|
|||||||
Timeline.PublicFederated -> api.publicTimeline(local = false, maxId = maxId, minId = minId, limit = loadSize)
|
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.PublicLocal -> api.publicTimeline(local = true, maxId = maxId, minId = minId, limit = loadSize)
|
||||||
Timeline.TrendingStatuses -> api.trendingStatuses(limit = LIMIT_TRENDING_STATUSES)
|
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 -> {
|
is Timeline.Hashtags -> {
|
||||||
val firstHashtag = timeline.tags.first()
|
val firstHashtag = timeline.tags.first()
|
||||||
val additionalHashtags = timeline.tags.subList(1, timeline.tags.size)
|
val additionalHashtags = timeline.tags.subList(1, timeline.tags.size)
|
||||||
|
@ -29,9 +29,15 @@ class TrendingLinkViewHolder(
|
|||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
internal lateinit var link: TrendsLink
|
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
|
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.
|
* Each item shows an action to open the link.
|
||||||
*
|
*
|
||||||
* If present, an item to show the author's account is also included.
|
* 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(
|
internal class TrendingLinksAccessibilityDelegate(
|
||||||
private val recyclerView: RecyclerView,
|
private val recyclerView: RecyclerView,
|
||||||
@ -59,6 +62,11 @@ internal class TrendingLinksAccessibilityDelegate(
|
|||||||
context.getString(R.string.action_open_byline_account),
|
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) {
|
private val delegate = object : ItemDelegate(this) {
|
||||||
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
||||||
super.onInitializeAccessibilityNodeInfo(host, info)
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
||||||
@ -72,6 +80,10 @@ internal class TrendingLinksAccessibilityDelegate(
|
|||||||
viewHolder.link.authors?.firstOrNull()?.account?.let {
|
viewHolder.link.authors?.firstOrNull()?.account?.let {
|
||||||
info.addAction(openBylineAccountAction)
|
info.addAction(openBylineAccountAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((recyclerView.adapter as? TrendingLinksAdapter)?.showTimelineLink == true) {
|
||||||
|
info.addAction(openTimelineLinkAction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
|
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
|
||||||
@ -93,6 +105,11 @@ internal class TrendingLinksAccessibilityDelegate(
|
|||||||
listener.onClick(viewHolder.link, Target.BYLINE)
|
listener.onClick(viewHolder.link, Target.BYLINE)
|
||||||
true
|
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)
|
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.databinding.ItemTrendingLinkBinding
|
||||||
import app.pachli.view.PreviewCardView
|
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(
|
class TrendingLinksAdapter(
|
||||||
statusDisplayOptions: StatusDisplayOptions,
|
statusDisplayOptions: StatusDisplayOptions,
|
||||||
|
showTimelineLink: Boolean,
|
||||||
private val onViewLink: PreviewCardView.OnClickListener,
|
private val onViewLink: PreviewCardView.OnClickListener,
|
||||||
) : ListAdapter<TrendsLink, TrendingLinkViewHolder>(diffCallback) {
|
) : ListAdapter<TrendsLink, TrendingLinkViewHolder>(diffCallback) {
|
||||||
var statusDisplayOptions = statusDisplayOptions
|
var statusDisplayOptions = statusDisplayOptions
|
||||||
@ -37,6 +44,14 @@ class TrendingLinksAdapter(
|
|||||||
notifyItemRangeChanged(0, itemCount)
|
notifyItemRangeChanged(0, itemCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showTimelineLink = showTimelineLink
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
notifyItemRangeChanged(0, itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
||||||
}
|
}
|
||||||
@ -49,7 +64,7 @@ class TrendingLinksAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: TrendingLinkViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: TrendingLinkViewHolder, position: Int) {
|
||||||
holder.bind(getItem(position), statusDisplayOptions)
|
holder.bind(getItem(position), statusDisplayOptions, showTimelineLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
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.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
import app.pachli.core.designsystem.R as DR
|
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.AccountActivityIntent
|
||||||
|
import app.pachli.core.navigation.TimelineActivityIntent
|
||||||
import app.pachli.core.network.model.PreviewCard
|
import app.pachli.core.network.model.PreviewCard
|
||||||
import app.pachli.core.ui.ActionButtonScrollListener
|
import app.pachli.core.ui.ActionButtonScrollListener
|
||||||
import app.pachli.core.ui.BackgroundMessage
|
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.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||||
|
import io.github.z4kn4fein.semver.constraints.toConstraint
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -74,7 +79,13 @@ class TrendingLinksFragment :
|
|||||||
RefreshableFragment,
|
RefreshableFragment,
|
||||||
MenuProvider {
|
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)
|
private val binding by viewBinding(FragmentTrendingLinksBinding::bind)
|
||||||
|
|
||||||
@ -99,11 +110,6 @@ class TrendingLinksFragment :
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
|
|
||||||
trendingLinksAdapter = TrendingLinksAdapter(viewModel.statusDisplayOptions.value, ::onOpenLink)
|
|
||||||
|
|
||||||
setupSwipeRefreshLayout()
|
|
||||||
setupRecyclerView()
|
|
||||||
|
|
||||||
(activity as? ActionButtonActivity)?.actionButton?.let { actionButton ->
|
(activity as? ActionButtonActivity)?.actionButton?.let { actionButton ->
|
||||||
actionButton.show()
|
actionButton.show()
|
||||||
|
|
||||||
@ -119,68 +125,91 @@ class TrendingLinksFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trendingLinksAdapter = TrendingLinksAdapter(
|
||||||
|
viewModel.statusDisplayOptions.value,
|
||||||
|
false,
|
||||||
|
::onOpenLink,
|
||||||
|
)
|
||||||
|
|
||||||
|
setupSwipeRefreshLayout()
|
||||||
|
setupRecyclerView()
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.loadState.collectLatest {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
when (it) {
|
launch {
|
||||||
LoadState.Initial -> {
|
viewModel.pachliAccountFlow.distinctUntilChangedBy { it.server }.collect { account ->
|
||||||
viewModel.accept(InfallibleUiAction.Reload)
|
trendingLinksAdapter.showTimelineLink = account.server.can(
|
||||||
|
ServerOperation.ORG_JOINMASTODON_TIMELINES_LINK,
|
||||||
|
">=1.0.0".toConstraint(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LoadState.Loading -> {
|
launch {
|
||||||
if (!binding.swipeRefreshLayout.isRefreshing) {
|
viewModel.loadState.collect {
|
||||||
binding.progressBar.show()
|
when (it) {
|
||||||
} else {
|
LoadState.Loading -> bindLoading()
|
||||||
binding.progressBar.hide()
|
is LoadState.Success -> bindSuccess(it)
|
||||||
|
is LoadState.Error -> bindError(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is LoadState.Success -> {
|
launch {
|
||||||
trendingLinksAdapter.submitList(it.data)
|
viewModel.statusDisplayOptions.collectLatest {
|
||||||
binding.progressBar.hide()
|
trendingLinksAdapter.statusDisplayOptions = it
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
if (it.data.isEmpty()) {
|
|
||||||
binding.messageView.setup(BackgroundMessage.Empty())
|
|
||||||
binding.messageView.show()
|
|
||||||
} else {
|
|
||||||
binding.messageView.hide()
|
|
||||||
binding.recyclerView.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is 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",
|
|
||||||
Snackbar.LENGTH_INDEFINITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (it.throwable !is HttpException || it.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) {
|
|
||||||
viewModel.accept(InfallibleUiAction.Reload)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.messageView.setup(it.throwable)
|
|
||||||
}
|
|
||||||
binding.messageView.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewModel.accept(InfallibleUiAction.Reload)
|
||||||
viewModel.statusDisplayOptions.collectLatest {
|
}
|
||||||
trendingLinksAdapter.statusDisplayOptions = it
|
|
||||||
|
private fun bindLoading() {
|
||||||
|
if (!binding.swipeRefreshLayout.isRefreshing) {
|
||||||
|
binding.progressBar.show()
|
||||||
|
} else {
|
||||||
|
binding.progressBar.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindSuccess(loadState: LoadState.Success) {
|
||||||
|
trendingLinksAdapter.submitList(loadState.data)
|
||||||
|
binding.progressBar.hide()
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
if (loadState.data.isEmpty()) {
|
||||||
|
binding.messageView.setup(BackgroundMessage.Empty())
|
||||||
|
binding.messageView.show()
|
||||||
|
} else {
|
||||||
|
binding.messageView.hide()
|
||||||
|
binding.recyclerView.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
loadState.throwable.message ?: "Error",
|
||||||
|
Snackbar.LENGTH_INDEFINITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (loadState.throwable !is HttpException || loadState.throwable.code() != 404) {
|
||||||
|
snackbar.setAction("Retry") { viewModel.accept(InfallibleUiAction.Reload) }
|
||||||
}
|
}
|
||||||
|
snackbar.show()
|
||||||
|
} else {
|
||||||
|
if (loadState.throwable !is HttpException || loadState.throwable.code() != 404) {
|
||||||
|
binding.messageView.setup(loadState.throwable) {
|
||||||
|
viewModel.accept(InfallibleUiAction.Reload)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.messageView.setup(loadState.throwable)
|
||||||
|
}
|
||||||
|
binding.messageView.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,14 +267,22 @@ class TrendingLinksFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onOpenLink(card: PreviewCard, target: Target) {
|
private fun onOpenLink(card: PreviewCard, target: Target) {
|
||||||
if (target == Target.BYLINE) {
|
when (target) {
|
||||||
card.authors?.firstOrNull()?.account?.id?.let {
|
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))
|
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() {
|
override fun onResume() {
|
||||||
|
@ -19,7 +19,9 @@ package app.pachli.components.trending.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.pachli.components.account.AccountViewModel
|
||||||
import app.pachli.components.trending.TrendingLinksRepository
|
import app.pachli.components.trending.TrendingLinksRepository
|
||||||
|
import app.pachli.core.common.extensions.stateFlow
|
||||||
import app.pachli.core.common.extensions.throttleFirst
|
import app.pachli.core.common.extensions.throttleFirst
|
||||||
import app.pachli.core.data.repository.AccountManager
|
import app.pachli.core.data.repository.AccountManager
|
||||||
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
|
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.PrefKeys
|
||||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
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.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed interface UiAction
|
sealed interface UiAction
|
||||||
@ -50,23 +54,38 @@ sealed interface InfallibleUiAction : UiAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface LoadState {
|
sealed interface LoadState {
|
||||||
data object Initial : LoadState
|
|
||||||
data object Loading : LoadState
|
data object Loading : LoadState
|
||||||
data class Success(val data: List<TrendsLink>) : LoadState
|
data class Success(val data: List<TrendsLink>) : LoadState
|
||||||
data class Error(val throwable: Throwable) : LoadState
|
data class Error(val throwable: Throwable) : LoadState
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel(assistedFactory = TrendingLinksViewModel.Factory::class)
|
||||||
class TrendingLinksViewModel @Inject constructor(
|
class TrendingLinksViewModel @AssistedInject constructor(
|
||||||
|
@Assisted private val pachliAccountId: Long,
|
||||||
private val repository: TrendingLinksRepository,
|
private val repository: TrendingLinksRepository,
|
||||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
) : ViewModel() {
|
) : 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)
|
private val reload = MutableSharedFlow<Unit>(replay = 1)
|
||||||
val loadState = _loadState.asStateFlow()
|
|
||||||
|
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
|
val showFabWhileScrolling = sharedPreferencesRepository.changes
|
||||||
.filter { it == null || it == PrefKeys.FAB_HIDE }
|
.filter { it == null || it == PrefKeys.FAB_HIDE }
|
||||||
@ -85,17 +104,14 @@ class TrendingLinksViewModel @Inject constructor(
|
|||||||
uiAction
|
uiAction
|
||||||
.throttleFirst()
|
.throttleFirst()
|
||||||
.filterIsInstance<InfallibleUiAction.Reload>()
|
.filterIsInstance<InfallibleUiAction.Reload>()
|
||||||
.onEach { invalidate() }
|
.onEach { reload.emit(Unit) }
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun invalidate() = viewModelScope.launch {
|
@AssistedFactory
|
||||||
_loadState.update { LoadState.Loading }
|
interface Factory {
|
||||||
val response = repository.getTrendingLinks()
|
/** Creates [AccountViewModel] with [pachliAccountId] as the active account. */
|
||||||
response.fold(
|
fun create(pachliAccountId: Long): TrendingLinksViewModel
|
||||||
{ list -> _loadState.update { LoadState.Success(list) } },
|
|
||||||
{ throwable -> _loadState.update { LoadState.Error(throwable) } },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,19 +21,21 @@ import android.content.Context
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.core.activity.decodeBlurHash
|
import app.pachli.core.activity.decodeBlurHash
|
||||||
import app.pachli.core.activity.emojify
|
import app.pachli.core.activity.emojify
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.string.unicodeWrap
|
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.data.model.StatusDisplayOptions
|
||||||
import app.pachli.core.designsystem.R as DR
|
import app.pachli.core.designsystem.R as DR
|
||||||
import app.pachli.core.network.model.PreviewCard
|
import app.pachli.core.network.model.PreviewCard
|
||||||
|
import app.pachli.core.network.model.TrendsLink
|
||||||
import app.pachli.databinding.PreviewCardBinding
|
import app.pachli.databinding.PreviewCardBinding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.MultiTransformation
|
import com.bumptech.glide.load.MultiTransformation
|
||||||
@ -66,6 +68,9 @@ class PreviewCardView @JvmOverloads constructor(
|
|||||||
|
|
||||||
/** The author byline */
|
/** The author byline */
|
||||||
BYLINE,
|
BYLINE,
|
||||||
|
|
||||||
|
/** The link timeline */
|
||||||
|
TIMELINE_LINK,
|
||||||
}
|
}
|
||||||
|
|
||||||
fun interface OnClickListener {
|
fun interface OnClickListener {
|
||||||
@ -114,12 +119,15 @@ class PreviewCardView @JvmOverloads constructor(
|
|||||||
* @param card The card to bind
|
* @param card The card to bind
|
||||||
* @param sensitive True if the status that contained this card was marked sensitive
|
* @param sensitive True if the status that contained this card was marked sensitive
|
||||||
* @param statusDisplayOptions
|
* @param statusDisplayOptions
|
||||||
|
* @param showTimelineLink True if the UI to view a timeline of statuses about this link
|
||||||
|
* should be shown.
|
||||||
* @param listener
|
* @param listener
|
||||||
*/
|
*/
|
||||||
fun bind(
|
fun bind(
|
||||||
card: PreviewCard,
|
card: PreviewCard,
|
||||||
sensitive: Boolean,
|
sensitive: Boolean,
|
||||||
statusDisplayOptions: StatusDisplayOptions,
|
statusDisplayOptions: StatusDisplayOptions,
|
||||||
|
showTimelineLink: Boolean,
|
||||||
listener: OnClickListener,
|
listener: OnClickListener,
|
||||||
): Unit = with(binding) {
|
): Unit = with(binding) {
|
||||||
cardTitle.text = card.title
|
cardTitle.text = card.title
|
||||||
@ -135,9 +143,8 @@ class PreviewCardView @JvmOverloads constructor(
|
|||||||
|
|
||||||
previewCardWrapper.setOnClickListener { listener.onClick(card, Target.CARD) }
|
previewCardWrapper.setOnClickListener { listener.onClick(card, Target.CARD) }
|
||||||
cardImage.setOnClickListener { listener.onClick(card, Target.IMAGE) }
|
cardImage.setOnClickListener { listener.onClick(card, Target.IMAGE) }
|
||||||
byline.referencedIds.forEach { id ->
|
authorInfo.setOnClickListener { listener.onClick(card, Target.BYLINE) }
|
||||||
root.findViewById<View>(id).setOnClickListener { listener.onClick(card, Target.BYLINE) }
|
timelineLink.setOnClickListener { listener.onClick(card, Target.TIMELINE_LINK) }
|
||||||
}
|
|
||||||
|
|
||||||
cardLink.text = card.url
|
cardLink.text = card.url
|
||||||
|
|
||||||
@ -177,14 +184,60 @@ class PreviewCardView @JvmOverloads constructor(
|
|||||||
cardImage.hide()
|
cardImage.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
card.authors?.firstOrNull()?.account?.let { account ->
|
var showBylineDivider = false
|
||||||
val name = account.name.unicodeWrap().emojify(account.emojis, authorInfo, false)
|
bylineDivider.hide()
|
||||||
authorInfo.text = authorInfo.context.getString(R.string.preview_card_byline_fmt, name)
|
|
||||||
|
|
||||||
Glide.with(authorInfo.context).load(account.avatar).transform(bylineAvatarTransformation)
|
// Determine how to show the author info (if present)
|
||||||
.placeholder(DR.drawable.avatar_default).into(bylineAvatarTarget)
|
val author = card.authors?.firstOrNull()
|
||||||
byline.show()
|
when {
|
||||||
} ?: byline.hide()
|
// 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)
|
||||||
|
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 */
|
/** Adjusts the layout parameters to place the image above the information views */
|
||||||
|
@ -126,12 +126,25 @@
|
|||||||
tools:ignore="SelectableText"
|
tools:ignore="SelectableText"
|
||||||
tools:text="@tools:sample/lorem" />
|
tools:text="@tools:sample/lorem" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Group
|
<TextView
|
||||||
android:id="@+id/byline"
|
android:id="@+id/timeline_link"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="visible"
|
android:background="?selectableItemBackground"
|
||||||
app:constraint_referenced_ids="byline_divider,author_info" />
|
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>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</merge>
|
</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_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_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="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_byline_account">Mostrar el perfil del autor del artículo</string>
|
||||||
<string name="action_open_link">Abrir enlace</string>
|
<string name="action_open_link">Abrir enlace</string>
|
||||||
<string name="compose_warn_language_dialog_title">Comprueba el idioma de la publicación</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_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_prepare_media_unknown_mime_type">tuntematon tiedostotyyppi</string>
|
||||||
<string name="error_pick_media_fmt">Julkaisuun ei voitu liittää tiedostoa: %1$s</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_byline_account">Näytä julkaisun tekijän profiili</string>
|
||||||
<string name="action_open_link">Avaa linkki</string>
|
<string name="action_open_link">Avaa linkki</string>
|
||||||
<string name="error_prepare_media_unknown_file_size">tiedoston kokoa ei voitu määrittää</string>
|
<string name="error_prepare_media_unknown_file_size">tiedoston kokoa ei voitu määrittää</string>
|
||||||
@ -793,4 +793,4 @@
|
|||||||
<string name="main_viewmodel_error_refresh_account">Tilin päivittäminen epäonnistui, virhe oli:\n\n%1$s\n\nVoit jatkaa, mutta listasi ja suodattimesi saattavat olla eivät ehkä ole ajan tasalla.</string>
|
<string name="main_viewmodel_error_refresh_account">Tilin päivittäminen epäonnistui, virhe oli:\n\n%1$s\n\nVoit jatkaa, mutta listasi ja suodattimesi saattavat olla eivät ehkä ole ajan tasalla.</string>
|
||||||
<string name="action_relogin">Kirjaudu uudestaan</string>
|
<string name="action_relogin">Kirjaudu uudestaan</string>
|
||||||
<string name="upload_failed_modify_attachment">Muokkaa liitettä</string>
|
<string name="upload_failed_modify_attachment">Muokkaa liitettä</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -692,7 +692,7 @@
|
|||||||
<string name="pref_title_notification_method">Modh fógartha</string>
|
<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_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="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_byline_account">Taispeáin próifíl údar an ailt</string>
|
||||||
<string name="action_open_link">Oscail nasc</string>
|
<string name="action_open_link">Oscail nasc</string>
|
||||||
<string name="search_operator_attachment_dialog_title">Teorainn le poist leis na meáin?</string>
|
<string name="search_operator_attachment_dialog_title">Teorainn le poist leis na meáin?</string>
|
||||||
@ -824,4 +824,4 @@
|
|||||||
<string name="filter_action_none">Taispeáin</string>
|
<string name="filter_action_none">Taispeáin</string>
|
||||||
<string name="pref_account_notification_filters_label_not_followed">… ní leanann tú</string>
|
<string name="pref_account_notification_filters_label_not_followed">… ní leanann tú</string>
|
||||||
<string name="account_filter_placeholder_type_follow_fmt">Lean úsáideoir @<b>%1s</b> tú</string>
|
<string name="account_filter_placeholder_type_follow_fmt">Lean úsáideoir @<b>%1s</b> tú</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -626,7 +626,7 @@
|
|||||||
<string name="error_prepare_media_unknown_mime_type">tipo de ficheiro descoñecido</string>
|
<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_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="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_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="error_save_filter_failed_fmt">Non se gardou o filtro: %1$s</string>
|
||||||
<string name="search_operator_attachment_dialog_image_label">Imaxes</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="search_operator_attachment_no_media_label">Ingen media</string>
|
||||||
<string name="error_prepare_media_unknown_mime_type">filtypen er ukjent</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="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_byline_account">Vis artikkelforfatterens profil</string>
|
||||||
<string name="action_open_link">Åpne lenk</string>
|
<string name="action_open_link">Åpne lenk</string>
|
||||||
<string name="error_load_filter_failed_fmt">Innlasting av filter mislyktes: %1$s</string>
|
<string name="error_load_filter_failed_fmt">Innlasting av filter mislyktes: %1$s</string>
|
||||||
@ -777,4 +777,4 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="main_viewmodel_error_refresh_account">Å gjenoppfriske kontoen mislyktes med den følgende feilen:\n\n%1$s\n\nDu kan fortsette, men det kan hende at dine lister og filtere kan være ufullstendige.</string>
|
<string name="main_viewmodel_error_refresh_account">Å gjenoppfriske kontoen mislyktes med den følgende feilen:\n\n%1$s\n\nDu kan fortsette, men det kan hende at dine lister og filtere kan være ufullstendige.</string>
|
||||||
<string name="action_relogin">Logg inn på nytt</string>
|
<string name="action_relogin">Logg inn på nytt</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -733,10 +733,14 @@
|
|||||||
<string name="error_prepare_media_unknown_mime_type">file\'s type is not known</string>
|
<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="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_byline_account">Show article author\'s profile</string>
|
||||||
<string name="action_open_link">Open link</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_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_save_filter_failed_fmt">Saving filter failed: %1$s</string>
|
||||||
<string name="error_delete_filter_failed_fmt">Deleting 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_SEARCH_QUERY_LANGUAGE
|
||||||
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED
|
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_STATUSES_TRANSLATE
|
||||||
|
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_TIMELINES_LINK
|
||||||
import app.pachli.core.network.R
|
import app.pachli.core.network.R
|
||||||
import app.pachli.core.network.model.InstanceV1
|
import app.pachli.core.network.model.InstanceV1
|
||||||
import app.pachli.core.network.model.InstanceV2
|
import app.pachli.core.network.model.InstanceV2
|
||||||
@ -333,6 +334,11 @@ data class Server(
|
|||||||
c[ORG_JOINMASTODON_SEARCH_QUERY_FROM] = "1.0.0".toVersion()
|
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 -> {
|
GOTOSOCIAL -> {
|
||||||
|
@ -137,6 +137,7 @@ enum class FilterContext {
|
|||||||
Timeline.TrendingStatuses,
|
Timeline.TrendingStatuses,
|
||||||
Timeline.TrendingHashtags,
|
Timeline.TrendingHashtags,
|
||||||
Timeline.TrendingLinks,
|
Timeline.TrendingLinks,
|
||||||
|
is Timeline.Link,
|
||||||
-> PUBLIC
|
-> PUBLIC
|
||||||
Timeline.Conversations -> null
|
Timeline.Conversations -> null
|
||||||
}
|
}
|
||||||
|
@ -111,4 +111,13 @@ enum class ServerOperation(id: String, versions: List<Version>) {
|
|||||||
Version(major = 1),
|
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")
|
@TypeLabel("trending_statuses")
|
||||||
data object TrendingStatuses : Timeline
|
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: DRAFTS
|
||||||
|
|
||||||
// TODO: SCHEDULED
|
// TODO: SCHEDULED
|
||||||
|
@ -585,6 +585,16 @@ class TimelineActivityIntent private constructor(context: Context, pachliAccount
|
|||||||
putExtra(EXTRA_TIMELINE, Timeline.Hashtags(listOf(hashtag)))
|
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.
|
* Show statuses from a list.
|
||||||
*
|
*
|
||||||
|
@ -73,6 +73,7 @@ enum class FilterContext {
|
|||||||
Timeline.TrendingStatuses,
|
Timeline.TrendingStatuses,
|
||||||
Timeline.TrendingHashtags,
|
Timeline.TrendingHashtags,
|
||||||
Timeline.TrendingLinks,
|
Timeline.TrendingLinks,
|
||||||
|
is Timeline.Link,
|
||||||
-> PUBLIC
|
-> PUBLIC
|
||||||
Timeline.Conversations -> null
|
Timeline.Conversations -> null
|
||||||
}
|
}
|
||||||
|
@ -152,6 +152,14 @@ interface MastodonApi {
|
|||||||
@Query("limit") limit: Int? = null,
|
@Query("limit") limit: Int? = null,
|
||||||
): Response<List<Status>>
|
): 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")
|
@GET("api/v1/notifications")
|
||||||
suspend fun notifications(
|
suspend fun notifications(
|
||||||
/** Return results older than this ID */
|
/** Return results older than this ID */
|
||||||
|
@ -46,6 +46,9 @@
|
|||||||
<!-- Open the link in a preview card -->
|
<!-- Open the link in a preview card -->
|
||||||
<item name="action_open_link" type="id" />
|
<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 -->
|
<!-- Copy the item -->
|
||||||
<item name="action_copy_item" type="id" />
|
<item name="action_copy_item" type="id" />
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user