Timeline refactor (#2175)
* Move Timeline files into their own package * Introduce TimelineViewModel, add coroutines * Simplify StatusViewData * Handle timeilne fetch errors * Rework filters, fix ViewThreadFragment * Fix NotificationsFragment * Simplify Notifications and Thread, handle pin * Redo loading in TimelineViewModel * Improve error handling in TimelineViewModel * Rewrite actions in TimelineViewModel * Apply feedback after timeline factoring review * Handle initial failure in timeline correctly
This commit is contained in:
parent
0a992480c2
commit
44a5b42cac
58 changed files with 3956 additions and 3618 deletions
|
|
@ -0,0 +1,56 @@
|
|||
package com.keylesspalace.tusky.network
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* One-stop for status filtering logic using Mastodon's filters.
|
||||
*
|
||||
* 1. You init with [initWithFilters], this compiles regex pattern.
|
||||
* 2. You call [shouldFilterStatus] to figure out what to display when you load statuses.
|
||||
*/
|
||||
class FilterModel @Inject constructor() {
|
||||
private var pattern: Pattern? = null
|
||||
|
||||
fun initWithFilters(filters: List<Filter>) {
|
||||
this.pattern = makeFilter(filters)
|
||||
}
|
||||
|
||||
fun shouldFilterStatus(status: Status): Boolean {
|
||||
// Patterns are expensive and thread-safe, matchers are neither.
|
||||
val matcher = pattern?.matcher("") ?: return false
|
||||
|
||||
if (status.poll != null) {
|
||||
val pollMatches = status.poll.options.any { matcher.reset(it.title).find() }
|
||||
if (pollMatches) return true
|
||||
}
|
||||
|
||||
val spoilerText = status.actionableStatus.spoilerText
|
||||
return (matcher.reset(status.actionableStatus.content).find() ||
|
||||
spoilerText.isNotEmpty() && matcher.reset(spoilerText).find())
|
||||
}
|
||||
|
||||
private fun filterToRegexToken(filter: Filter): String? {
|
||||
val phrase = filter.phrase
|
||||
val quotedPhrase = Pattern.quote(phrase)
|
||||
return if (filter.wholeWord && ALPHANUMERIC.matcher(phrase).matches()) {
|
||||
String.format("(^|\\W)%s($|\\W)", quotedPhrase)
|
||||
} else {
|
||||
quotedPhrase
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeFilter(filters: List<Filter>): Pattern? {
|
||||
if (filters.isEmpty()) return null
|
||||
val tokens = filters.map { filterToRegexToken(it) }
|
||||
|
||||
return Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ALPHANUMERIC = Pattern.compile("^\\w+$")
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ interface MastodonApi {
|
|||
fun getInstance(): Single<Instance>
|
||||
|
||||
@GET("api/v1/filters")
|
||||
fun getFilters(): Call<List<Filter>>
|
||||
fun getFilters(): Single<List<Filter>>
|
||||
|
||||
@GET("api/v1/timelines/home")
|
||||
fun homeTimeline(
|
||||
|
|
|
|||
|
|
@ -30,20 +30,20 @@ import java.lang.IllegalStateException
|
|||
*/
|
||||
|
||||
interface TimelineCases {
|
||||
fun reblog(status: Status, reblog: Boolean): Single<Status>
|
||||
fun favourite(status: Status, favourite: Boolean): Single<Status>
|
||||
fun bookmark(status: Status, bookmark: Boolean): Single<Status>
|
||||
fun mute(id: String, notifications: Boolean, duration: Int?)
|
||||
fun block(id: String)
|
||||
fun delete(id: String): Single<DeletedStatus>
|
||||
fun pin(status: Status, pin: Boolean)
|
||||
fun voteInPoll(status: Status, choices: List<Int>): Single<Poll>
|
||||
fun muteConversation(status: Status, mute: Boolean): Single<Status>
|
||||
fun reblog(statusId: String, reblog: Boolean): Single<Status>
|
||||
fun favourite(statusId: String, favourite: Boolean): Single<Status>
|
||||
fun bookmark(statusId: String, bookmark: Boolean): Single<Status>
|
||||
fun mute(statusId: String, notifications: Boolean, duration: Int?)
|
||||
fun block(statusId: String)
|
||||
fun delete(statusId: String): Single<DeletedStatus>
|
||||
fun pin(statusId: String, pin: Boolean): Single<Status>
|
||||
fun voteInPoll(statusId: String, pollId: String, choices: List<Int>): Single<Poll>
|
||||
fun muteConversation(statusId: String, mute: Boolean): Single<Status>
|
||||
}
|
||||
|
||||
class TimelineCasesImpl(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
) : TimelineCases {
|
||||
|
||||
/**
|
||||
|
|
@ -52,103 +52,92 @@ class TimelineCasesImpl(
|
|||
*/
|
||||
private val cancelDisposable = CompositeDisposable()
|
||||
|
||||
override fun reblog(status: Status, reblog: Boolean): Single<Status> {
|
||||
val id = status.actionableId
|
||||
|
||||
override fun reblog(statusId: String, reblog: Boolean): Single<Status> {
|
||||
val call = if (reblog) {
|
||||
mastodonApi.reblogStatus(id)
|
||||
mastodonApi.reblogStatus(statusId)
|
||||
} else {
|
||||
mastodonApi.unreblogStatus(id)
|
||||
mastodonApi.unreblogStatus(statusId)
|
||||
}
|
||||
return call.doAfterSuccess {
|
||||
eventHub.dispatch(ReblogEvent(status.id, reblog))
|
||||
eventHub.dispatch(ReblogEvent(statusId, reblog))
|
||||
}
|
||||
}
|
||||
|
||||
override fun favourite(status: Status, favourite: Boolean): Single<Status> {
|
||||
val id = status.actionableId
|
||||
|
||||
override fun favourite(statusId: String, favourite: Boolean): Single<Status> {
|
||||
val call = if (favourite) {
|
||||
mastodonApi.favouriteStatus(id)
|
||||
mastodonApi.favouriteStatus(statusId)
|
||||
} else {
|
||||
mastodonApi.unfavouriteStatus(id)
|
||||
mastodonApi.unfavouriteStatus(statusId)
|
||||
}
|
||||
return call.doAfterSuccess {
|
||||
eventHub.dispatch(FavoriteEvent(status.id, favourite))
|
||||
eventHub.dispatch(FavoriteEvent(statusId, favourite))
|
||||
}
|
||||
}
|
||||
|
||||
override fun bookmark(status: Status, bookmark: Boolean): Single<Status> {
|
||||
val id = status.actionableId
|
||||
|
||||
override fun bookmark(statusId: String, bookmark: Boolean): Single<Status> {
|
||||
val call = if (bookmark) {
|
||||
mastodonApi.bookmarkStatus(id)
|
||||
mastodonApi.bookmarkStatus(statusId)
|
||||
} else {
|
||||
mastodonApi.unbookmarkStatus(id)
|
||||
mastodonApi.unbookmarkStatus(statusId)
|
||||
}
|
||||
return call.doAfterSuccess {
|
||||
eventHub.dispatch(BookmarkEvent(status.id, bookmark))
|
||||
eventHub.dispatch(BookmarkEvent(statusId, bookmark))
|
||||
}
|
||||
}
|
||||
|
||||
override fun muteConversation(status: Status, mute: Boolean): Single<Status> {
|
||||
val id = status.actionableId
|
||||
|
||||
override fun muteConversation(statusId: String, mute: Boolean): Single<Status> {
|
||||
val call = if (mute) {
|
||||
mastodonApi.muteConversation(id)
|
||||
mastodonApi.muteConversation(statusId)
|
||||
} else {
|
||||
mastodonApi.unmuteConversation(id)
|
||||
mastodonApi.unmuteConversation(statusId)
|
||||
}
|
||||
return call.doAfterSuccess {
|
||||
eventHub.dispatch(MuteConversationEvent(status.id, mute))
|
||||
eventHub.dispatch(MuteConversationEvent(statusId, mute))
|
||||
}
|
||||
}
|
||||
|
||||
override fun mute(id: String, notifications: Boolean, duration: Int?) {
|
||||
mastodonApi.muteAccount(id, notifications, duration)
|
||||
.subscribe({
|
||||
eventHub.dispatch(MuteEvent(id))
|
||||
}, { t ->
|
||||
Log.w("Failed to mute account", t)
|
||||
})
|
||||
.addTo(cancelDisposable)
|
||||
override fun mute(statusId: String, notifications: Boolean, duration: Int?) {
|
||||
mastodonApi.muteAccount(statusId, notifications, duration)
|
||||
.subscribe({
|
||||
eventHub.dispatch(MuteEvent(statusId))
|
||||
}, { t ->
|
||||
Log.w("Failed to mute account", t)
|
||||
})
|
||||
.addTo(cancelDisposable)
|
||||
}
|
||||
|
||||
override fun block(id: String) {
|
||||
mastodonApi.blockAccount(id)
|
||||
.subscribe({
|
||||
eventHub.dispatch(BlockEvent(id))
|
||||
}, { t ->
|
||||
Log.w("Failed to block account", t)
|
||||
})
|
||||
.addTo(cancelDisposable)
|
||||
override fun block(statusId: String) {
|
||||
mastodonApi.blockAccount(statusId)
|
||||
.subscribe({
|
||||
eventHub.dispatch(BlockEvent(statusId))
|
||||
}, { t ->
|
||||
Log.w("Failed to block account", t)
|
||||
})
|
||||
.addTo(cancelDisposable)
|
||||
}
|
||||
|
||||
override fun delete(id: String): Single<DeletedStatus> {
|
||||
return mastodonApi.deleteStatus(id)
|
||||
.doAfterSuccess {
|
||||
eventHub.dispatch(StatusDeletedEvent(id))
|
||||
}
|
||||
override fun delete(statusId: String): Single<DeletedStatus> {
|
||||
return mastodonApi.deleteStatus(statusId)
|
||||
.doAfterSuccess {
|
||||
eventHub.dispatch(StatusDeletedEvent(statusId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun pin(status: Status, pin: Boolean) {
|
||||
override fun pin(statusId: String, pin: Boolean): Single<Status> {
|
||||
// Replace with extension method if we use RxKotlin
|
||||
(if (pin) mastodonApi.pinStatus(status.id) else mastodonApi.unpinStatus(status.id))
|
||||
.subscribe({ updatedStatus ->
|
||||
status.pinned = updatedStatus.pinned
|
||||
}, {})
|
||||
.addTo(this.cancelDisposable)
|
||||
return (if (pin) mastodonApi.pinStatus(statusId) else mastodonApi.unpinStatus(statusId))
|
||||
.doAfterSuccess {
|
||||
eventHub.dispatch(PinEvent(statusId, pin))
|
||||
}
|
||||
}
|
||||
|
||||
override fun voteInPoll(status: Status, choices: List<Int>): Single<Poll> {
|
||||
val pollId = status.actionableStatus.poll?.id
|
||||
|
||||
if(pollId == null || choices.isEmpty()) {
|
||||
override fun voteInPoll(statusId: String, pollId: String, choices: List<Int>): Single<Poll> {
|
||||
if (choices.isEmpty()) {
|
||||
return Single.error(IllegalStateException())
|
||||
}
|
||||
|
||||
return mastodonApi.voteInPoll(pollId, choices).doAfterSuccess {
|
||||
eventHub.dispatch(PollVoteEvent(status.id, it))
|
||||
eventHub.dispatch(PollVoteEvent(statusId, it))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue