Redesign report activity (#1295)
* Report activity core * Implement navigation * Implement navigation * Update strings * Revert manifest formatting * Implement Done page * Add landscape layout * Implement Note fragment * Create component * Implement simple status adapter * Format code * Add date/time to report statuses * Refactor status view holder * Refactor code * Refactor ViewPager * Replace MaterialButton with Button * Remove unneeded string * Update Text and Check views style * Remove old ReportActivity and rename Report2Activity to ReportActivity * Hide "report to remote instance" checkbox for local accounts * Add account, hashtag and links click handler * Add media preview * Add sensitive content support * Add status expand/collapse support * Update adapter to user adapterPosition instead of stored status * Updated checked change handling * Add polls support to report screen * Add copyright * Set buttonTint at CheckBox * Exclude reblogs from statuses for reports * Change final page check mark size * Update report note screen * Fix typos * Remove unused params from api endpoint * Replace .visibility with show()/hide() * Replace Date().time with System.currentTime... * Add line spacing * Fix close button tint issue * Updated status adapter
This commit is contained in:
parent
f7581daa75
commit
c335651b6b
39 changed files with 2726 additions and 416 deletions
|
@ -0,0 +1,162 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Spanned
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.HtmlUtils
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.support.HasSupportFragmentInjector
|
||||
import kotlinx.android.synthetic.main.activity_report.*
|
||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class ReportActivity : BottomSheetActivity(), HasSupportFragmentInjector {
|
||||
|
||||
@Inject
|
||||
lateinit var dispatchingFragmentInjector: DispatchingAndroidInjector<Fragment>
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: ReportViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory)[ReportViewModel::class.java]
|
||||
val accountId = intent?.getStringExtra(ACCOUNT_ID)
|
||||
val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME)
|
||||
if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) {
|
||||
throw IllegalStateException("accountId ($accountId) or accountUserName ($accountUserName) is null")
|
||||
}
|
||||
|
||||
viewModel.init(accountId, accountUserName,
|
||||
intent?.getStringExtra(STATUS_ID), intent?.getStringExtra(STATUS_CONTENT))
|
||||
|
||||
|
||||
setContentView(R.layout.activity_report)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
val closeIcon = AppCompatResources.getDrawable(this, R.drawable.ic_close_24dp)
|
||||
ThemeUtils.setDrawableTint(this, closeIcon!!, R.attr.compose_close_button_tint)
|
||||
|
||||
supportActionBar?.apply {
|
||||
title = getString(R.string.report_username_format, viewModel.accountUserName)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
setHomeAsUpIndicator(closeIcon)
|
||||
}
|
||||
|
||||
initViewPager()
|
||||
if (savedInstanceState == null) {
|
||||
viewModel.navigateTo(Screen.Statuses)
|
||||
}
|
||||
subscribeObservables()
|
||||
}
|
||||
|
||||
private fun initViewPager() {
|
||||
wizard.adapter = ReportPagerAdapter(supportFragmentManager)
|
||||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
viewModel.navigation.observe(this, Observer { screen ->
|
||||
if (screen != null) {
|
||||
viewModel.navigated()
|
||||
when (screen) {
|
||||
Screen.Statuses -> showStatusesPage()
|
||||
Screen.Note -> showNotesPage()
|
||||
Screen.Done -> showDonePage()
|
||||
Screen.Back -> showPreviousScreen()
|
||||
Screen.Finish -> closeScreen()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.checkUrl.observe(this, Observer {
|
||||
if (!it.isNullOrBlank()) {
|
||||
viewModel.urlChecked()
|
||||
viewUrl(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun showPreviousScreen() {
|
||||
when (wizard.currentItem) {
|
||||
0 -> closeScreen()
|
||||
1 -> showStatusesPage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDonePage() {
|
||||
wizard.currentItem = 2
|
||||
}
|
||||
|
||||
private fun showNotesPage() {
|
||||
wizard.currentItem = 1
|
||||
}
|
||||
|
||||
private fun closeScreen() {
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showStatusesPage() {
|
||||
wizard.currentItem = 0
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
closeScreen()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACCOUNT_ID = "account_id"
|
||||
private const val ACCOUNT_USERNAME = "account_username"
|
||||
private const val STATUS_ID = "status_id"
|
||||
private const val STATUS_CONTENT = "status_content"
|
||||
|
||||
@JvmStatic
|
||||
fun getIntent(context: Context, accountId: String, userName: String, statusId: String, statusContent: Spanned) =
|
||||
Intent(context, ReportActivity::class.java)
|
||||
.apply {
|
||||
putExtra(ACCOUNT_ID, accountId)
|
||||
putExtra(ACCOUNT_USERNAME, userName)
|
||||
putExtra(STATUS_ID, statusId)
|
||||
putExtra(STATUS_CONTENT, HtmlUtils.toHtml(statusContent))
|
||||
}
|
||||
}
|
||||
|
||||
override fun supportFragmentInjector(): AndroidInjector<Fragment> = dispatchingFragmentInjector
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.PagedList
|
||||
import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
|
||||
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 io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val statusesRepository: StatusesRepository) : ViewModel() {
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val navigationMutable = MutableLiveData<Screen>()
|
||||
val navigation: LiveData<Screen> = navigationMutable
|
||||
|
||||
private val muteStateMutable = MutableLiveData<Resource<Boolean>>()
|
||||
val muteState: LiveData<Resource<Boolean>> = muteStateMutable
|
||||
|
||||
private val blockStateMutable = MutableLiveData<Resource<Boolean>>()
|
||||
val blockState: LiveData<Resource<Boolean>> = blockStateMutable
|
||||
|
||||
private val reportingStateMutable = MutableLiveData<Resource<Boolean>>()
|
||||
var reportingState: LiveData<Resource<Boolean>> = reportingStateMutable
|
||||
|
||||
private val checkUrlMutable = MutableLiveData<String>()
|
||||
val checkUrl: LiveData<String> = 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 }
|
||||
|
||||
private val selectedIds = HashSet<String>()
|
||||
val statusViewState = StatusViewState()
|
||||
|
||||
var reportNote: String? = null
|
||||
var isRemoteNotify = false
|
||||
|
||||
private var statusContent: String? = null
|
||||
private var statusId: String? = null
|
||||
lateinit var accountUserName: String
|
||||
lateinit var accountId: String
|
||||
var isRemoteAccount: Boolean = false
|
||||
var remoteServer: String? = null
|
||||
|
||||
fun init(accountId: String, userName: String, statusId: String?, statusContent: String?) {
|
||||
this.accountId = accountId
|
||||
this.accountUserName = userName
|
||||
this.statusId = statusId
|
||||
statusId?.let {
|
||||
selectedIds.add(it)
|
||||
}
|
||||
this.statusContent = statusContent
|
||||
isRemoteAccount = userName.contains('@')
|
||||
if (isRemoteAccount) {
|
||||
remoteServer = userName.substring(userName.indexOf('@') + 1)
|
||||
}
|
||||
|
||||
obtainRelationship()
|
||||
repoResult.value = statusesRepository.getStatuses(accountId, statusId, disposables)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun navigateTo(screen: Screen) {
|
||||
navigationMutable.value = screen
|
||||
}
|
||||
|
||||
fun navigated() {
|
||||
navigationMutable.value = null
|
||||
}
|
||||
|
||||
|
||||
private fun obtainRelationship() {
|
||||
val ids = listOf(accountId)
|
||||
muteStateMutable.value = Loading()
|
||||
blockStateMutable.value = Loading()
|
||||
disposables.add(
|
||||
mastodonApi.relationshipsObservable(ids)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ data ->
|
||||
updateRelationship(data.getOrNull(0))
|
||||
|
||||
},
|
||||
{
|
||||
updateRelationship(null)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
private fun updateRelationship(relationship: Relationship?) {
|
||||
if (relationship != null) {
|
||||
muteStateMutable.value = Success(relationship.muting)
|
||||
blockStateMutable.value = Success(relationship.blocking)
|
||||
} else {
|
||||
muteStateMutable.value = Error(false)
|
||||
blockStateMutable.value = Error(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleMute() {
|
||||
val single: Single<Relationship> = if (muteStateMutable.value?.data == true) {
|
||||
mastodonApi.unmuteAccountObservable(accountId)
|
||||
} else {
|
||||
mastodonApi.muteAccountObservable(accountId)
|
||||
}
|
||||
muteStateMutable.value = Loading()
|
||||
disposables.add(
|
||||
single
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ relationship ->
|
||||
muteStateMutable.value = Success(relationship?.muting == true)
|
||||
},
|
||||
{ error ->
|
||||
muteStateMutable.value = Error(false, error.message)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fun toggleBlock() {
|
||||
val single: Single<Relationship> = if (blockStateMutable.value?.data == true) {
|
||||
mastodonApi.unblockAccountObservable(accountId)
|
||||
} else {
|
||||
mastodonApi.blockAccountObservable(accountId)
|
||||
}
|
||||
blockStateMutable.value = Loading()
|
||||
disposables.add(
|
||||
single
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ relationship ->
|
||||
blockStateMutable.value = Success(relationship?.blocking == true)
|
||||
},
|
||||
{ error ->
|
||||
blockStateMutable.value = Error(false, error.message)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fun doReport() {
|
||||
reportingStateMutable.value = Loading()
|
||||
disposables.add(
|
||||
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)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun retryStatusLoad() {
|
||||
repoResult.value?.retry?.invoke()
|
||||
}
|
||||
|
||||
fun refreshStatuses() {
|
||||
repoResult.value?.refresh?.invoke()
|
||||
}
|
||||
|
||||
fun checkClickedUrl(url: String?) {
|
||||
checkUrlMutable.value = url
|
||||
}
|
||||
|
||||
fun urlChecked() {
|
||||
checkUrlMutable.value = null
|
||||
}
|
||||
|
||||
fun setStatusChecked(status: Status, checked: Boolean) {
|
||||
if (checked)
|
||||
selectedIds.add(status.id)
|
||||
else
|
||||
selectedIds.remove(status.id)
|
||||
}
|
||||
|
||||
fun isStatusChecked(id: String): Boolean {
|
||||
return selectedIds.contains(id)
|
||||
}
|
||||
|
||||
fun isStatusesSelected(): Boolean {
|
||||
return selectedIds.isNotEmpty()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report
|
||||
|
||||
enum class Screen {
|
||||
Statuses,
|
||||
Note,
|
||||
Done,
|
||||
Back,
|
||||
Finish
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import android.view.View
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import java.util.ArrayList
|
||||
|
||||
interface AdapterHandler: LinkListener {
|
||||
fun showMedia(v: View?, status: Status?, idx: Int)
|
||||
fun setStatusChecked(status: Status, isChecked: Boolean)
|
||||
fun isStatusChecked(id: String): Boolean
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
|
||||
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
|
||||
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
|
||||
|
||||
class ReportPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manager) {
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> ReportStatusesFragment.newInstance()
|
||||
1 -> ReportNoteFragment.newInstance()
|
||||
2 -> ReportDoneFragment.newInstance()
|
||||
else -> throw IllegalArgumentException("Unknown page index: $position")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCount(): Int = 3
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
|
||||
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
|
||||
import kotlinx.android.synthetic.main.item_report_status.view.*
|
||||
import java.util.*
|
||||
|
||||
class StatusViewHolder(itemView: View,
|
||||
private val useAbsoluteTime: Boolean,
|
||||
private val mediaPreviewEnabled: Boolean,
|
||||
private val viewState: StatusViewState,
|
||||
private val adapterHandler: AdapterHandler,
|
||||
private val getStatusForPosition: (Int) -> Status?) : RecyclerView.ViewHolder(itemView) {
|
||||
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
||||
private val statusViewHelper = StatusViewHelper(itemView)
|
||||
|
||||
private val previewListener = object : StatusViewHelper.MediaPreviewListener {
|
||||
override fun onViewMedia(v: View?, idx: Int) {
|
||||
status()?.let { status ->
|
||||
adapterHandler.showMedia(v, status, idx)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean) {
|
||||
status()?.id?.let { id ->
|
||||
viewState.setMediaShow(id, isShowing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
itemView.statusSelection.setOnCheckedChangeListener { _, isChecked ->
|
||||
status()?.let { status ->
|
||||
adapterHandler.setStatusChecked(status, isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(status: Status) {
|
||||
itemView.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id)
|
||||
|
||||
updateTextView()
|
||||
|
||||
val sensitive = status.sensitive
|
||||
|
||||
statusViewHelper.setMediasPreview(mediaPreviewEnabled, status.attachments, sensitive, previewListener,
|
||||
viewState.isMediaShow(status.id, status.sensitive),
|
||||
mediaViewHeight)
|
||||
|
||||
statusViewHelper.setupPollReadonly(status.poll, status.emojis, useAbsoluteTime)
|
||||
setCreatedAt(status.createdAt)
|
||||
}
|
||||
|
||||
private fun updateTextView() {
|
||||
status()?.let { status ->
|
||||
setupCollapsedState(status.isCollapsible(), viewState.isCollapsed(status.id, true),
|
||||
viewState.isContentShow(status.id, status.sensitive), status.spoilerText)
|
||||
|
||||
if (status.spoilerText.isBlank()) {
|
||||
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler)
|
||||
itemView.statusContentWarningButton.hide()
|
||||
itemView.statusContentWarningDescription.hide()
|
||||
} else {
|
||||
val emojiSpoiler = CustomEmojiHelper.emojifyString(status.spoilerText, status.emojis, itemView.statusContentWarningDescription)
|
||||
itemView.statusContentWarningDescription.text = emojiSpoiler
|
||||
itemView.statusContentWarningDescription.show()
|
||||
itemView.statusContentWarningButton.show()
|
||||
itemView.statusContentWarningButton.isChecked = viewState.isContentShow(status.id, true)
|
||||
itemView.statusContentWarningButton.setOnCheckedChangeListener { _, isViewChecked ->
|
||||
status()?.let { status ->
|
||||
itemView.statusContentWarningDescription.invalidate()
|
||||
viewState.setContentShow(status.id, isViewChecked)
|
||||
setTextVisible(isViewChecked, status.content, status.mentions, status.emojis, adapterHandler)
|
||||
}
|
||||
}
|
||||
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setTextVisible(expanded: Boolean,
|
||||
content: Spanned,
|
||||
mentions: Array<Status.Mention>?,
|
||||
emojis: List<Emoji>,
|
||||
listener: LinkListener) {
|
||||
if (expanded) {
|
||||
val emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, itemView.statusContent)
|
||||
LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener)
|
||||
} else {
|
||||
LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener)
|
||||
}
|
||||
if (itemView.statusContent.text.isNullOrBlank()) {
|
||||
itemView.statusContent.hide()
|
||||
} else {
|
||||
itemView.statusContent.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCreatedAt(createdAt: Date?) {
|
||||
if (useAbsoluteTime) {
|
||||
itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
|
||||
} else {
|
||||
itemView.timestampInfo.text = if (createdAt != null) {
|
||||
val then = createdAt.time
|
||||
val now = System.currentTimeMillis()
|
||||
DateUtils.getRelativeTimeSpanString(itemView.timestampInfo.context, then, now)
|
||||
} else {
|
||||
// unknown minutes~
|
||||
"?m"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) {
|
||||
/* input filter for TextViews have to be set before text */
|
||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||
itemView.buttonToggleContent.setOnCheckedChangeListener { _, isChecked ->
|
||||
status()?.let { status ->
|
||||
viewState.setCollapsed(status.id, isChecked)
|
||||
updateTextView()
|
||||
}
|
||||
}
|
||||
|
||||
itemView.buttonToggleContent.show()
|
||||
if (collapsed) {
|
||||
itemView.buttonToggleContent.isChecked = true
|
||||
itemView.statusContent.filters = COLLAPSE_INPUT_FILTER
|
||||
} else {
|
||||
itemView.buttonToggleContent.isChecked = false
|
||||
itemView.statusContent.filters = NO_INPUT_FILTER
|
||||
}
|
||||
} else {
|
||||
itemView.buttonToggleContent.hide()
|
||||
itemView.statusContent.filters = NO_INPUT_FILTER
|
||||
}
|
||||
}
|
||||
|
||||
private fun status() = getStatusForPosition(adapterPosition)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
||||
class StatusesAdapter(private val useAbsoluteTime: Boolean,
|
||||
private val mediaPreviewEnabled: Boolean,
|
||||
private val statusViewState: StatusViewState,
|
||||
private val adapterHandler: AdapterHandler)
|
||||
: PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
private val statusForPosition: (Int) -> Status? = { position: Int ->
|
||||
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return StatusViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_report_status, parent, false),
|
||||
useAbsoluteTime, mediaPreviewEnabled, statusViewState, adapterHandler, statusForPosition)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
getItem(position)?.let { status ->
|
||||
(holder as? StatusViewHolder)?.bind(status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
|
||||
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.ItemKeyedDataSource
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class StatusesDataSource(private val accountId: String,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor) : ItemKeyedDataSource<String, Status>() {
|
||||
|
||||
val networkStateAfter = MutableLiveData<NetworkState>()
|
||||
val networkStateBefore = MutableLiveData<NetworkState>()
|
||||
|
||||
private var retryAfter: (() -> Any)? = null
|
||||
private var retryBefore: (() -> Any)? = null
|
||||
private var retryInitial: (() -> Any)? = null
|
||||
|
||||
val initialLoad = MutableLiveData<NetworkState>()
|
||||
fun retryAllFailed() {
|
||||
var prevRetry = retryInitial
|
||||
retryInitial = null
|
||||
prevRetry?.let {
|
||||
retryExecutor.execute {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
prevRetry = retryAfter
|
||||
retryAfter = null
|
||||
prevRetry?.let {
|
||||
retryExecutor.execute {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
prevRetry = retryBefore
|
||||
retryBefore = null
|
||||
prevRetry?.let {
|
||||
retryExecutor.execute {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<Status>) {
|
||||
networkStateAfter.postValue(NetworkState.LOADED)
|
||||
networkStateBefore.postValue(NetworkState.LOADED)
|
||||
retryAfter = null
|
||||
retryBefore = null
|
||||
retryInitial = null
|
||||
initialLoad.postValue(NetworkState.LOADING)
|
||||
mastodonApi.statusObservable(params.requestedInitialKey).zipWith(
|
||||
mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true),
|
||||
BiFunction { status: Status, list: List<Status> ->
|
||||
val ret = ArrayList<Status>()
|
||||
ret.add(status)
|
||||
ret.addAll(list)
|
||||
return@BiFunction ret
|
||||
})
|
||||
.doOnSubscribe {
|
||||
disposables.add(it)
|
||||
}
|
||||
.subscribe(
|
||||
{
|
||||
callback.onResult(it)
|
||||
initialLoad.postValue(NetworkState.LOADED)
|
||||
},
|
||||
{
|
||||
retryInitial = {
|
||||
loadInitial(params, callback)
|
||||
}
|
||||
initialLoad.postValue(NetworkState.error(it.message))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Status>) {
|
||||
networkStateAfter.postValue(NetworkState.LOADING)
|
||||
retryAfter = null
|
||||
mastodonApi.accountStatusesObservable(accountId, params.key, null, params.requestedLoadSize, true)
|
||||
.doOnSubscribe {
|
||||
disposables.add(it)
|
||||
}
|
||||
.subscribe(
|
||||
{
|
||||
callback.onResult(it)
|
||||
networkStateAfter.postValue(NetworkState.LOADED)
|
||||
},
|
||||
{
|
||||
retryAfter = {
|
||||
loadAfter(params, callback)
|
||||
}
|
||||
networkStateAfter.postValue(NetworkState.error(it.message))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Status>) {
|
||||
networkStateBefore.postValue(NetworkState.LOADING)
|
||||
retryBefore = null
|
||||
mastodonApi.accountStatusesObservable(accountId, null, params.key, params.requestedLoadSize, true)
|
||||
.doOnSubscribe {
|
||||
disposables.add(it)
|
||||
}
|
||||
.subscribe(
|
||||
{
|
||||
callback.onResult(it)
|
||||
networkStateBefore.postValue(NetworkState.LOADED)
|
||||
},
|
||||
{
|
||||
retryBefore = {
|
||||
loadBefore(params, callback)
|
||||
}
|
||||
networkStateBefore.postValue(NetworkState.error(it.message))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getKey(item: Status): String = item.id
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class StatusesDataSourceFactory(
|
||||
private val accountId: String,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor) : DataSource.Factory<String, Status>() {
|
||||
val sourceLiveData = MutableLiveData<StatusesDataSource>()
|
||||
override fun create(): DataSource<String, Status> {
|
||||
val source = StatusesDataSource(accountId, mastodonApi, disposables, retryExecutor)
|
||||
sourceLiveData.postValue(source)
|
||||
return source
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.Config
|
||||
import androidx.paging.toLiveData
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.BiListing
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class StatusesRepository @Inject constructor(private val mastodonApi: MastodonApi) {
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
fun getStatuses(accountId: String, initialStatus: String?, disposables: CompositeDisposable, pageSize: Int = 20): BiListing<Status> {
|
||||
val sourceFactory = StatusesDataSourceFactory(accountId, mastodonApi, disposables, executor)
|
||||
val livePagedList = sourceFactory.toLiveData(
|
||||
config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2),
|
||||
fetchExecutor = executor, initialLoadKey = initialStatus
|
||||
)
|
||||
return BiListing(
|
||||
pagedList = livePagedList,
|
||||
networkStateBefore = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.networkStateBefore
|
||||
},
|
||||
networkStateAfter = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.networkStateAfter
|
||||
},
|
||||
retry = {
|
||||
sourceFactory.sourceLiveData.value?.retryAllFailed()
|
||||
},
|
||||
refresh = {
|
||||
sourceFactory.sourceLiveData.value?.invalidate()
|
||||
},
|
||||
refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.initialLoad
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.fragments
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.report.Screen
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import kotlinx.android.synthetic.main.fragment_report_done.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class ReportDoneFragment : Fragment(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: ReportViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_report_done, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName)
|
||||
handleClicks()
|
||||
subscribeObservables()
|
||||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
viewModel.muteState.observe(viewLifecycleOwner, Observer {
|
||||
if (it !is Loading) {
|
||||
buttonMute.show()
|
||||
progressMute.show()
|
||||
} else {
|
||||
buttonMute.hide()
|
||||
progressMute.hide()
|
||||
}
|
||||
|
||||
buttonMute.setText(when {
|
||||
it.data == true -> R.string.action_unmute
|
||||
else -> R.string.action_mute
|
||||
})
|
||||
})
|
||||
|
||||
viewModel.blockState.observe(viewLifecycleOwner, Observer {
|
||||
if (it !is Loading) {
|
||||
buttonBlock.show()
|
||||
progressBlock.show()
|
||||
}
|
||||
else{
|
||||
buttonBlock.hide()
|
||||
progressBlock.hide()
|
||||
}
|
||||
buttonBlock.setText(when {
|
||||
it.data == true -> R.string.action_unblock
|
||||
else -> R.string.action_block
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun handleClicks() {
|
||||
buttonDone.setOnClickListener {
|
||||
viewModel.navigateTo(Screen.Finish)
|
||||
}
|
||||
buttonBlock.setOnClickListener {
|
||||
viewModel.toggleBlock()
|
||||
}
|
||||
buttonMute.setOnClickListener {
|
||||
viewModel.toggleMute()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = ReportDoneFragment()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.report.Screen
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import kotlinx.android.synthetic.main.fragment_report_note.*
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportNoteFragment : Fragment(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: ReportViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_report_note, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
fillViews()
|
||||
handleChanges()
|
||||
handleClicks()
|
||||
subscribeObservables()
|
||||
}
|
||||
|
||||
private fun handleChanges() {
|
||||
editNote.doAfterTextChanged {
|
||||
viewModel.reportNote = it?.toString()
|
||||
}
|
||||
checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked ->
|
||||
viewModel.isRemoteNotify = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillViews() {
|
||||
editNote.setText(viewModel.reportNote)
|
||||
|
||||
if (viewModel.isRemoteAccount){
|
||||
checkIsNotifyRemote.show()
|
||||
reportDescriptionRemoteInstance.show()
|
||||
}
|
||||
else{
|
||||
checkIsNotifyRemote.hide()
|
||||
reportDescriptionRemoteInstance.hide()
|
||||
}
|
||||
|
||||
if (viewModel.isRemoteAccount)
|
||||
checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer)
|
||||
checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify
|
||||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
viewModel.reportingState.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
is Success -> viewModel.navigateTo(Screen.Done)
|
||||
is Loading -> showLoading()
|
||||
is Error -> showError(it.cause)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun showError(error: Throwable?) {
|
||||
editNote.isEnabled = true
|
||||
checkIsNotifyRemote.isEnabled = true
|
||||
buttonReport.isEnabled = true
|
||||
buttonBack.isEnabled = true
|
||||
progressBar.hide()
|
||||
|
||||
Snackbar.make(buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.apply {
|
||||
setAction(R.string.action_retry) {
|
||||
sendReport()
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun sendReport() {
|
||||
viewModel.doReport()
|
||||
}
|
||||
|
||||
private fun showLoading() {
|
||||
buttonReport.isEnabled = false
|
||||
buttonBack.isEnabled = false
|
||||
editNote.isEnabled = false
|
||||
checkIsNotifyRemote.isEnabled = false
|
||||
progressBar.show()
|
||||
}
|
||||
|
||||
private fun handleClicks() {
|
||||
buttonBack.setOnClickListener {
|
||||
viewModel.navigateTo(Screen.Back)
|
||||
}
|
||||
|
||||
buttonReport.setOnClickListener {
|
||||
sendReport()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = ReportNoteFragment()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.report.Screen
|
||||
import com.keylesspalace.tusky.components.report.adapter.AdapterHandler
|
||||
import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import kotlinx.android.synthetic.main.fragment_report_statuses.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
private lateinit var viewModel: ReportViewModel
|
||||
|
||||
private lateinit var adapter: StatusesAdapter
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
|
||||
private var snackbarErrorRetry: Snackbar? = null
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
|
||||
}
|
||||
|
||||
override fun showMedia(v: View?, status: Status?, idx: Int) {
|
||||
status?.actionableStatus?.let { actionable ->
|
||||
when (actionable.attachments[idx].type) {
|
||||
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE -> {
|
||||
val attachments = AttachmentViewData.list(actionable)
|
||||
val intent = ViewMediaActivity.newIntent(context, attachments,
|
||||
idx)
|
||||
if (v != null) {
|
||||
val url = actionable.attachments[idx].url
|
||||
ViewCompat.setTransitionName(v, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(),
|
||||
v, url)
|
||||
startActivity(intent, options.toBundle())
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
Attachment.Type.UNKNOWN -> {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_report_statuses, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
handleClicks()
|
||||
initStatusesView()
|
||||
setupSwipeRefreshLayout()
|
||||
}
|
||||
|
||||
private fun setupSwipeRefreshLayout() {
|
||||
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground))
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
snackbarErrorRetry?.dismiss()
|
||||
viewModel.refreshStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initStatusesView() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||
|
||||
val account = accountManager.activeAccount
|
||||
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
|
||||
|
||||
|
||||
adapter = StatusesAdapter(useAbsoluteTime, mediaPreviewEnabled, viewModel.statusViewState, this)
|
||||
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.adapter = adapter
|
||||
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
viewModel.statuses.observe(viewLifecycleOwner, Observer<PagedList<Status>> {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
|
||||
viewModel.networkStateAfter.observe(viewLifecycleOwner, Observer {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
progressBarBottom.show()
|
||||
else
|
||||
progressBarBottom.hide()
|
||||
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
})
|
||||
|
||||
viewModel.networkStateBefore.observe(viewLifecycleOwner, Observer {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
progressBarTop.show()
|
||||
else
|
||||
progressBarTop.hide()
|
||||
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
})
|
||||
|
||||
viewModel.networkStateRefresh.observe(viewLifecycleOwner, Observer {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing)
|
||||
progressBarLoading.show()
|
||||
else
|
||||
progressBarLoading.hide()
|
||||
|
||||
if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
|
||||
if (snackbarErrorRetry?.isShown != true) {
|
||||
snackbarErrorRetry = Snackbar.make(swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
|
||||
snackbarErrorRetry?.setAction(R.string.action_retry) {
|
||||
viewModel.retryStatusLoad()
|
||||
}
|
||||
snackbarErrorRetry?.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleClicks() {
|
||||
buttonCancel.setOnClickListener {
|
||||
viewModel.navigateTo(Screen.Back)
|
||||
}
|
||||
|
||||
buttonContinue.setOnClickListener {
|
||||
if (viewModel.isStatusesSelected()) {
|
||||
viewModel.navigateTo(Screen.Note)
|
||||
} else {
|
||||
Snackbar.make(swipeRefreshLayout, R.string.error_report_too_few_statuses, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setStatusChecked(status: Status, isChecked: Boolean) {
|
||||
viewModel.setStatusChecked(status, isChecked)
|
||||
}
|
||||
|
||||
override fun isStatusChecked(id: String): Boolean {
|
||||
return viewModel.isStatusChecked(id)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
||||
|
||||
override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag))
|
||||
|
||||
override fun onViewUrl(url: String?) = viewModel.checkClickedUrl(url)
|
||||
|
||||
companion object {
|
||||
fun newInstance() = ReportStatusesFragment()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.model
|
||||
|
||||
class StatusViewState {
|
||||
private val mediaShownState = HashMap<String, Boolean>()
|
||||
private val contentShownState = HashMap<String, Boolean>()
|
||||
private val longContentCollapsedState = HashMap<String, Boolean>()
|
||||
|
||||
fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(mediaShownState, id, !isSensitive)
|
||||
fun setMediaShow(id: String, isShow: Boolean) = setStateEnabled(mediaShownState, id, isShow)
|
||||
|
||||
fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(contentShownState, id, !isSensitive)
|
||||
fun setContentShow(id: String, isShow: Boolean) = setStateEnabled(contentShownState, id, isShow)
|
||||
|
||||
fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(longContentCollapsedState, id, isCollapsed)
|
||||
fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed)
|
||||
|
||||
private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean = map[id]
|
||||
?: def
|
||||
|
||||
private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) = map.put(id, state)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue