Fix nullability problems

This commit is contained in:
Adolfo Santiago 2021-11-20 19:18:51 +01:00
parent 9e32d3c9bd
commit bf12d66423
No known key found for this signature in database
GPG key ID: 244D6F9A317B4A65
4 changed files with 292 additions and 244 deletions

View file

@ -34,13 +34,21 @@ import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.view.EmojiPicker
import kotlinx.android.synthetic.main.activity_announcements.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject
import kotlinx.android.synthetic.main.activity_announcements.announcementsList
import kotlinx.android.synthetic.main.activity_announcements.errorMessageView
import kotlinx.android.synthetic.main.activity_announcements.progressBar
import kotlinx.android.synthetic.main.activity_announcements.swipeRefreshLayout
import kotlinx.android.synthetic.main.toolbar_basic.toolbar
class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable {
class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
OnEmojiSelectedListener, Injectable {
@Inject
lateinit var viewModelFactory: ViewModelFactory
@ -52,13 +60,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
private val picker by lazy { EmojiPicker(this) }
private val pickerDialog by lazy {
PopupWindow(this)
.apply {
contentView = picker
isFocusable = true
setOnDismissListener {
currentAnnouncementId = null
}
.apply {
contentView = picker
isFocusable = true
setOnDismissListener {
currentAnnouncementId = null
}
}
}
private var currentAnnouncementId: String? = null
@ -89,12 +97,15 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
announcementsList.adapter = adapter
viewModel.announcements.observe(this) {
when (it) {
when(it) {
is Success -> {
progressBar.hide()
swipeRefreshLayout.isRefreshing = false
if (it.data.isNullOrEmpty()) {
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
if(it.data.isNullOrEmpty()) {
errorMessageView.setup(
R.drawable.elephant_friend_empty,
R.string.no_announcements
)
errorMessageView.show()
} else {
errorMessageView.hide()
@ -116,7 +127,9 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
}
viewModel.emojis.observe(this) {
picker.adapter = EmojiAdapter(it, this)
it?.let { list ->
picker.adapter = EmojiAdapter(list, this)
}
}
viewModel.load()
@ -124,7 +137,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
when(item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
@ -163,13 +176,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
}
override fun onViewAccount(id: String?) {
if (id != null) {
if(id != null) {
viewAccount(id)
}
}
override fun onViewUrl(url: String?) {
if (url != null) {
if(url != null) {
viewUrl(url)
}
}

View file

@ -27,159 +27,166 @@ import com.keylesspalace.tusky.entity.Announcement
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success
import io.reactivex.rxkotlin.Singles
import javax.inject.Inject
class AnnouncementsViewModel @Inject constructor(
accountManager: AccountManager,
private val appDatabase: AppDatabase,
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
accountManager: AccountManager,
private val appDatabase: AppDatabase,
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
) : RxAwareViewModel() {
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
private val emojisMutable = MutableLiveData<List<Emoji>>()
val emojis: LiveData<List<Emoji>> = emojisMutable
private val emojisMutable = MutableLiveData<List<Emoji>?>()
val emojis: LiveData<List<Emoji>?>
get() = emojisMutable
init {
Singles.zip(
mastodonApi.getCustomEmojis(),
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
.onErrorResumeNext(
mastodonApi.getInstance()
.map { Either.Right(it) }
)
mastodonApi.getCustomEmojis(),
appDatabase.instanceDao()
.loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
.onErrorResumeNext(
mastodonApi.getInstance()
.map { Either.Right(it) }
)
) { emojis, either ->
either.asLeftOrNull()?.copy(emojiList = emojis)
?: InstanceEntity(
accountManager.activeAccount?.domain!!,
emojis,
either.asRight().maxTootChars,
either.asRight().pollLimits?.maxOptions,
either.asRight().pollLimits?.maxOptionChars,
either.asRight().version,
either.asRight().chatLimit
)
?: InstanceEntity(
accountManager.activeAccount?.domain!!,
emojis,
either.asRight().maxTootChars,
either.asRight().pollLimits?.maxOptions,
either.asRight().pollLimits?.maxOptionChars,
either.asRight().version,
either.asRight().chatLimit
)
}
.doOnSuccess {
appDatabase.instanceDao().insertOrReplace(it)
}
.subscribe({
emojisMutable.postValue(it.emojiList)
}, {
Log.w(TAG, "Failed to get custom emojis.", it)
})
.autoDispose()
.doOnSuccess {
appDatabase.instanceDao().insertOrReplace(it)
}
.subscribe({ instanceEntity ->
emojisMutable.postValue(instanceEntity.emojiList)
}, {
Log.w(TAG, "Failed to get custom emojis.", it)
})
.autoDispose()
}
fun load() {
announcementsMutable.postValue(Loading())
mastodonApi.listAnnouncements()
.subscribe({
announcementsMutable.postValue(Success(it))
it.filter { announcement -> !announcement.read }
.forEach { announcement ->
mastodonApi.dismissAnnouncement(announcement.id)
.subscribe(
{
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
},
{ throwable ->
Log.d(TAG, "Failed to mark announcement as read.", throwable)
}
)
.autoDispose()
}
}, {
announcementsMutable.postValue(Error(cause = it))
})
.autoDispose()
.subscribe({
announcementsMutable.postValue(Success(it))
it.filter { announcement -> !announcement.read }
.forEach { announcement ->
mastodonApi.dismissAnnouncement(announcement.id)
.subscribe(
{
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
},
{ throwable ->
Log.d(TAG, "Failed to mark announcement as read.", throwable)
}
)
.autoDispose()
}
}, {
announcementsMutable.postValue(Error(cause = it))
})
.autoDispose()
}
fun addReaction(announcementId: String, name: String) {
mastodonApi.addAnnouncementReaction(announcementId, name)
.subscribe({
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) {
announcement.copy(
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
announcement.reactions.map { reaction ->
if (reaction.name == name) {
reaction.copy(
count = reaction.count + 1,
me = true
)
} else {
reaction
}
}
} else {
listOf(
*announcement.reactions.toTypedArray(),
emojis.value!!.find { emoji -> emoji.shortcode == name }
!!.run {
Announcement.Reaction(
name,
1,
true,
url,
staticUrl
)
}
)
}
)
} else {
announcement
.subscribe({
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
if(announcement.id == announcementId) {
announcement.copy(
reactions = if(announcement.reactions.find { reaction -> reaction.name == name } != null) {
announcement.reactions.map { reaction ->
if(reaction.name == name) {
reaction.copy(
count = reaction.count + 1,
me = true
)
} else {
reaction
}
}
} else {
listOf(
*announcement.reactions.toTypedArray(),
emojis.value!!.find { emoji -> emoji.shortcode == name }
!!.run {
Announcement.Reaction(
name,
1,
true,
url,
staticUrl
)
}
)
}
)
)
} else {
announcement
}
}
)
}, {
Log.w(TAG, "Failed to add reaction to the announcement.", it)
})
.autoDispose()
)
}, {
Log.w(TAG, "Failed to add reaction to the announcement.", it)
})
.autoDispose()
}
fun removeReaction(announcementId: String, name: String) {
mastodonApi.removeAnnouncementReaction(announcementId, name)
.subscribe({
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) {
announcement.copy(
reactions = announcement.reactions.mapNotNull { reaction ->
if (reaction.name == name) {
if (reaction.count > 1) {
reaction.copy(
count = reaction.count - 1,
me = false
)
} else {
null
}
} else {
reaction
}
}
)
.subscribe({
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
if(announcement.id == announcementId) {
announcement.copy(
reactions = announcement.reactions.mapNotNull { reaction ->
if(reaction.name == name) {
if(reaction.count > 1) {
reaction.copy(
count = reaction.count - 1,
me = false
)
} else {
null
}
} else {
announcement
reaction
}
}
)
)
} else {
announcement
}
}
)
}, {
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
})
.autoDispose()
)
}, {
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
})
.autoDispose()
}
companion object {

View file

@ -13,15 +13,18 @@ import com.keylesspalace.tusky.util.Listing
import com.keylesspalace.tusky.util.NetworkState
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.concurrent.Executors
import javax.inject.Inject
import javax.inject.Singleton
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@Singleton
class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, val db: AppDatabase) {
class ConversationsRepository @Inject constructor(
val mastodonApi: MastodonApi,
val db: AppDatabase
) {
private val ioExecutor = Executors.newSingleThreadExecutor()
@ -37,23 +40,26 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi,
}
mastodonApi.getConversations(limit = DEFAULT_PAGE_SIZE).enqueue(
object : Callback<List<Conversation>> {
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
// retrofit calls this on main thread so safe to call set value
networkState.value = NetworkState.error(t.message)
}
object : Callback<List<Conversation>> {
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
// retrofit calls this on main thread so safe to call set value
networkState.value = NetworkState.error(t.message)
}
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) {
ioExecutor.execute {
db.runInTransaction {
db.conversationDao().deleteForAccount(accountId)
insertResultIntoDb(accountId, response.body())
}
// since we are in bg thread now, post the result.
networkState.postValue(NetworkState.LOADED)
override fun onResponse(
call: Call<List<Conversation>>,
response: Response<List<Conversation>>
) {
ioExecutor.execute {
db.runInTransaction {
db.conversationDao().deleteForAccount(accountId)
insertResultIntoDb(accountId, response.body())
}
// since we are in bg thread now, post the result.
networkState.postValue(NetworkState.LOADED)
}
}
}
)
return networkState
}
@ -63,11 +69,12 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi,
// create a boundary callback which will observe when the user reaches to the edges of
// the list and update the database with extra data.
val boundaryCallback = ConversationsBoundaryCallback(
accountId = accountId,
mastodonApi = mastodonApi,
handleResponse = this::insertResultIntoDb,
ioExecutor = ioExecutor,
networkPageSize = DEFAULT_PAGE_SIZE)
accountId = accountId,
mastodonApi = mastodonApi,
handleResponse = this::insertResultIntoDb,
ioExecutor = ioExecutor,
networkPageSize = DEFAULT_PAGE_SIZE
)
// we are using a mutable live data to trigger refresh requests which eventually calls
// refresh method and gets a new live data. Each refresh request by the user becomes a newly
// dispatched data in refreshTrigger
@ -77,21 +84,25 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi,
}
// We use toLiveData Kotlin extension function here, you could also use LivePagedListBuilder
val livePagedList = db.conversationDao().conversationsForAccount(accountId).toLiveData(
config = Config(pageSize = DEFAULT_PAGE_SIZE, prefetchDistance = DEFAULT_PAGE_SIZE / 2, enablePlaceholders = false),
boundaryCallback = boundaryCallback
val livePagedList = db.conversationDao().conversationsForAccount(accountId).toLiveData(
config = Config(
pageSize = DEFAULT_PAGE_SIZE,
prefetchDistance = DEFAULT_PAGE_SIZE / 2,
enablePlaceholders = false
),
boundaryCallback = boundaryCallback
)
return Listing(
pagedList = livePagedList,
networkState = boundaryCallback.networkState,
retry = {
boundaryCallback.helper.retryAllFailed()
},
refresh = {
refreshTrigger.value = null
},
refreshState = refreshState
pagedList = livePagedList,
networkState = boundaryCallback.networkState,
retry = {
boundaryCallback.helper.retryAllFailed()
},
refresh = {
refreshTrigger.value = Unit
},
refreshState = refreshState
)
}
@ -99,13 +110,13 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi,
Single.fromCallable {
db.conversationDao().deleteForAccount(accountId)
}.subscribeOn(Schedulers.io())
.subscribe()
.subscribe()
}
private fun insertResultIntoDb(accountId: Long, result: List<Conversation>?) {
result?.filter { it.lastStatus != null }
?.map{ it.toEntity(accountId) }
?.let { db.conversationDao().insert(it) }
?.map { it.toEntity(accountId) }
?.let { db.conversationDao().insert(it) }
}
}
}

View file

@ -27,18 +27,26 @@ import com.keylesspalace.tusky.components.report.model.StatusViewState
import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.BiListing
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
class ReportViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
private val eventHub: EventHub,
private val statusesRepository: StatusesRepository) : RxAwareViewModel() {
private val mastodonApi: MastodonApi,
private val eventHub: EventHub,
private val statusesRepository: StatusesRepository
) : RxAwareViewModel() {
private val navigationMutable = MutableLiveData<Screen>()
val navigation: LiveData<Screen> = navigationMutable
private val navigationMutable = MutableLiveData<Screen?>()
val navigation: LiveData<Screen?>
get() = navigationMutable
private val muteStateMutable = MutableLiveData<Resource<Boolean>>()
val muteState: LiveData<Resource<Boolean>> = muteStateMutable
@ -49,14 +57,19 @@ class ReportViewModel @Inject constructor(
private val reportingStateMutable = MutableLiveData<Resource<Boolean>>()
var reportingState: LiveData<Resource<Boolean>> = reportingStateMutable
private val checkUrlMutable = MutableLiveData<String>()
val checkUrl: LiveData<String> = checkUrlMutable
private val checkUrlMutable = MutableLiveData<String?>()
val checkUrl: LiveData<String?>
get() = checkUrlMutable
private val repoResult = MutableLiveData<BiListing<Status>>()
val statuses: LiveData<PagedList<Status>> = Transformations.switchMap(repoResult) { it.pagedList }
val networkStateAfter: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateAfter }
val networkStateBefore: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateBefore }
val networkStateRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
val statuses: LiveData<PagedList<Status>> =
Transformations.switchMap(repoResult) { it.pagedList }
val networkStateAfter: LiveData<NetworkState> =
Transformations.switchMap(repoResult) { it.networkStateAfter }
val networkStateBefore: LiveData<NetworkState> =
Transformations.switchMap(repoResult) { it.networkStateBefore }
val networkStateRefresh: LiveData<NetworkState> =
Transformations.switchMap(repoResult) { it.refreshState }
private val selectedIds = HashSet<String>()
val statusViewState = StatusViewState()
@ -79,7 +92,7 @@ class ReportViewModel @Inject constructor(
}
isRemoteAccount = userName.contains('@')
if (isRemoteAccount) {
if(isRemoteAccount) {
remoteServer = userName.substring(userName.indexOf('@') + 1)
}
@ -95,29 +108,28 @@ class ReportViewModel @Inject constructor(
navigationMutable.value = null
}
private fun obtainRelationship() {
val ids = listOf(accountId)
muteStateMutable.value = Loading()
blockStateMutable.value = Loading()
mastodonApi.relationships(ids)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ data ->
updateRelationship(data.getOrNull(0))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ data ->
updateRelationship(data.getOrNull(0))
},
{
updateRelationship(null)
}
)
.autoDispose()
},
{
updateRelationship(null)
}
)
.autoDispose()
}
private fun updateRelationship(relationship: Relationship?) {
if (relationship != null) {
if(relationship != null) {
muteStateMutable.value = Success(relationship.muting)
blockStateMutable.value = Success(relationship.blocking)
} else {
@ -128,69 +140,74 @@ class ReportViewModel @Inject constructor(
fun toggleMute() {
val alreadyMuted = muteStateMutable.value?.data == true
if (alreadyMuted) {
if(alreadyMuted) {
mastodonApi.unmuteAccount(accountId)
} else {
mastodonApi.muteAccount(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val muting = relationship?.muting == true
muteStateMutable.value = Success(muting)
if (muting) {
eventHub.dispatch(MuteEvent(accountId, true))
}
},
{ error ->
muteStateMutable.value = Error(false, error.message)
}
).autoDispose()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val muting = relationship?.muting == true
muteStateMutable.value = Success(muting)
if(muting) {
eventHub.dispatch(MuteEvent(accountId, true))
}
},
{ error ->
muteStateMutable.value = Error(false, error.message)
}
).autoDispose()
muteStateMutable.value = Loading()
}
fun toggleBlock() {
val alreadyBlocked = blockStateMutable.value?.data == true
if (alreadyBlocked) {
if(alreadyBlocked) {
mastodonApi.unblockAccount(accountId)
} else {
mastodonApi.blockAccount(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val blocking = relationship?.blocking == true
blockStateMutable.value = Success(blocking)
if (blocking) {
eventHub.dispatch(BlockEvent(accountId))
}
},
{ error ->
blockStateMutable.value = Error(false, error.message)
}
)
.autoDispose()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val blocking = relationship?.blocking == true
blockStateMutable.value = Success(blocking)
if(blocking) {
eventHub.dispatch(BlockEvent(accountId))
}
},
{ error ->
blockStateMutable.value = Error(false, error.message)
}
)
.autoDispose()
blockStateMutable.value = Loading()
}
fun doReport() {
reportingStateMutable.value = Loading()
mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
reportingStateMutable.value = Success(true)
},
{ error ->
reportingStateMutable.value = Error(cause = error)
}
)
.autoDispose()
mastodonApi.reportObservable(
accountId,
selectedIds.toList(),
reportNote,
if(isRemoteAccount) isRemoteNotify else null
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
reportingStateMutable.value = Success(true)
},
{ error ->
reportingStateMutable.value = Error(cause = error)
}
)
.autoDispose()
}
@ -211,7 +228,7 @@ class ReportViewModel @Inject constructor(
}
fun setStatusChecked(status: Status, checked: Boolean) {
if (checked) {
if(checked) {
selectedIds.add(status.id)
} else {
selectedIds.remove(status.id)
@ -222,4 +239,4 @@ class ReportViewModel @Inject constructor(
return selectedIds.contains(id)
}
}
}