add status details
This commit is contained in:
parent
47afa8818f
commit
c5def59621
|
@ -48,6 +48,7 @@
|
||||||
<activity android:name=".components.about.licenses.LicenseActivity"/>
|
<activity android:name=".components.about.licenses.LicenseActivity"/>
|
||||||
<activity android:name="at.connyduck.pixelcat.components.compose.ComposeActivity" />
|
<activity android:name="at.connyduck.pixelcat.components.compose.ComposeActivity" />
|
||||||
<activity android:name=".components.profile.ProfileActivity" />
|
<activity android:name=".components.profile.ProfileActivity" />
|
||||||
|
<activity android:name=".components.timeline.detail.DetailActivity" />
|
||||||
|
|
||||||
<service android:name=".components.compose.SendStatusService" />
|
<service android:name=".components.compose.SendStatusService" />
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import at.connyduck.pixelcat.R
|
import at.connyduck.pixelcat.R
|
||||||
|
import at.connyduck.pixelcat.components.timeline.detail.DetailActivity
|
||||||
import at.connyduck.pixelcat.components.util.extension.getDisplayWidthInPx
|
import at.connyduck.pixelcat.components.util.extension.getDisplayWidthInPx
|
||||||
import at.connyduck.pixelcat.components.util.getColorForAttr
|
import at.connyduck.pixelcat.components.util.getColorForAttr
|
||||||
import at.connyduck.pixelcat.dagger.ViewModelFactory
|
import at.connyduck.pixelcat.dagger.ViewModelFactory
|
||||||
|
@ -101,4 +102,8 @@ class TimelineFragment : DaggerFragment(R.layout.fragment_timeline), TimeLineAct
|
||||||
override fun onMediaVisibilityChanged(status: StatusEntity) {
|
override fun onMediaVisibilityChanged(status: StatusEntity) {
|
||||||
viewModel.onMediaVisibilityChanged(status)
|
viewModel.onMediaVisibilityChanged(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetailsOpened(status: StatusEntity) {
|
||||||
|
startActivity(DetailActivity.newIntent(requireContext(), status.actionableId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import at.connyduck.pixelcat.databinding.ItemStatusBinding
|
||||||
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
import coil.api.load
|
import coil.api.load
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
interface TimeLineActionListener {
|
interface TimeLineActionListener {
|
||||||
|
@ -38,6 +39,7 @@ interface TimeLineActionListener {
|
||||||
fun onBoost(post: StatusEntity)
|
fun onBoost(post: StatusEntity)
|
||||||
fun onReply(status: StatusEntity)
|
fun onReply(status: StatusEntity)
|
||||||
fun onMediaVisibilityChanged(status: StatusEntity)
|
fun onMediaVisibilityChanged(status: StatusEntity)
|
||||||
|
fun onDetailsOpened(status: StatusEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
object TimelineDiffUtil : DiffUtil.ItemCallback<StatusEntity>() {
|
object TimelineDiffUtil : DiffUtil.ItemCallback<StatusEntity>() {
|
||||||
|
@ -66,65 +68,71 @@ class TimelineListAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemStatusBinding>, position: Int) {
|
override fun onBindViewHolder(holder: BindingHolder<ItemStatusBinding>, position: Int) {
|
||||||
|
|
||||||
getItem(position)?.let { status ->
|
getItem(position)?.let { status ->
|
||||||
|
holder.bind(status, displayWidth, listener, dateTimeFormatter)
|
||||||
// TODO order the stuff here
|
|
||||||
|
|
||||||
(holder.binding.postImages.adapter as TimelineImageAdapter).images = status.attachments
|
|
||||||
|
|
||||||
val maxImageRatio = status.attachments.map {
|
|
||||||
if(it.meta?.small?.width == null || it.meta.small.height == null) {
|
|
||||||
1f
|
|
||||||
} else {
|
|
||||||
it.meta.small.height.toFloat() / it.meta.small.width.toFloat()
|
|
||||||
}
|
|
||||||
}.max()?.coerceAtMost(1f) ?: 1f
|
|
||||||
|
|
||||||
holder.binding.postImages.layoutParams.height = (displayWidth * maxImageRatio).toInt()
|
|
||||||
|
|
||||||
holder.binding.postAvatar.load(status.account.avatar) {
|
|
||||||
transformations(RoundedCornersTransformation(25f))
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.postAvatar.setOnClickListener {
|
|
||||||
holder.binding.root.context.startActivity(ProfileActivity.newIntent(holder.binding.root.context, status.account.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.postDisplayName.text = status.account.displayName
|
|
||||||
holder.binding.postName.text = "@${status.account.username}"
|
|
||||||
|
|
||||||
holder.binding.postLikeButton.isChecked = status.favourited
|
|
||||||
|
|
||||||
holder.binding.postLikeButton.setEventListener { _, _ ->
|
|
||||||
listener.onFavorite(status)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.postBoostButton.isChecked = status.reblogged
|
|
||||||
|
|
||||||
holder.binding.postBoostButton.setEventListener { _, _ ->
|
|
||||||
listener.onBoost(status)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.postReplyButton.setOnClickListener {
|
|
||||||
listener.onReply(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.postIndicator.visible = status.attachments.size > 1
|
|
||||||
|
|
||||||
holder.binding.postImages.visible = status.attachments.isNotEmpty()
|
|
||||||
|
|
||||||
holder.binding.postDescription.text = status.content.parseAsHtml().trim()
|
|
||||||
|
|
||||||
holder.binding.postDate.text = dateTimeFormatter.format(status.createdAt)
|
|
||||||
|
|
||||||
holder.binding.postSensitiveMediaOverlay.visible = status.attachments.isNotEmpty() && !status.mediaVisible
|
|
||||||
|
|
||||||
holder.binding.postSensitiveMediaOverlay.setOnClickListener {
|
|
||||||
listener.onMediaVisibilityChanged(status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BindingHolder<ItemStatusBinding>.bind(status: StatusEntity, displayWidth: Int, listener: TimeLineActionListener, dateTimeFormatter: DateFormat) {
|
||||||
|
// TODO order the stuff here
|
||||||
|
|
||||||
|
(binding.postImages.adapter as TimelineImageAdapter).images = status.attachments
|
||||||
|
|
||||||
|
val maxImageRatio = status.attachments.map {
|
||||||
|
if(it.meta?.small?.width == null || it.meta.small.height == null) {
|
||||||
|
1f
|
||||||
|
} else {
|
||||||
|
it.meta.small.height.toFloat() / it.meta.small.width.toFloat()
|
||||||
|
}
|
||||||
|
}.max()?.coerceAtMost(1f) ?: 1f
|
||||||
|
|
||||||
|
binding.postImages.layoutParams.height = (displayWidth * maxImageRatio).toInt()
|
||||||
|
|
||||||
|
binding.postAvatar.load(status.account.avatar) {
|
||||||
|
transformations(RoundedCornersTransformation(25f))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.postAvatar.setOnClickListener {
|
||||||
|
binding.root.context.startActivity(ProfileActivity.newIntent(binding.root.context, status.account.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.postDisplayName.text = status.account.displayName
|
||||||
|
binding.postName.text = "@${status.account.username}"
|
||||||
|
|
||||||
|
binding.postLikeButton.isChecked = status.favourited
|
||||||
|
|
||||||
|
binding.postLikeButton.setEventListener { _, _ ->
|
||||||
|
listener.onFavorite(status)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.postBoostButton.isChecked = status.reblogged
|
||||||
|
|
||||||
|
binding.postBoostButton.setEventListener { _, _ ->
|
||||||
|
listener.onBoost(status)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.postReplyButton.setOnClickListener {
|
||||||
|
listener.onReply(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.postIndicator.visible = status.attachments.size > 1
|
||||||
|
|
||||||
|
binding.postImages.visible = status.attachments.isNotEmpty()
|
||||||
|
|
||||||
|
binding.postDescription.text = status.content.parseAsHtml().trim()
|
||||||
|
|
||||||
|
binding.postDate.text = dateTimeFormatter.format(status.createdAt)
|
||||||
|
|
||||||
|
binding.postSensitiveMediaOverlay.visible = status.attachments.isNotEmpty() && !status.mediaVisible
|
||||||
|
|
||||||
|
binding.postSensitiveMediaOverlay.setOnClickListener {
|
||||||
|
listener.onMediaVisibilityChanged(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
listener.onDetailsOpened(status)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package at.connyduck.pixelcat.components.timeline.detail
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.recyclerview.widget.MergeAdapter
|
||||||
|
import at.connyduck.pixelcat.components.general.BaseActivity
|
||||||
|
import at.connyduck.pixelcat.components.timeline.TimeLineActionListener
|
||||||
|
import at.connyduck.pixelcat.components.util.Success
|
||||||
|
import at.connyduck.pixelcat.components.util.extension.getDisplayWidthInPx
|
||||||
|
import at.connyduck.pixelcat.components.util.extension.hide
|
||||||
|
import at.connyduck.pixelcat.components.util.extension.show
|
||||||
|
import at.connyduck.pixelcat.dagger.ViewModelFactory
|
||||||
|
import at.connyduck.pixelcat.databinding.ActivityDetailBinding
|
||||||
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
|
import at.connyduck.pixelcat.util.viewBinding
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DetailActivity: BaseActivity(), TimeLineActionListener {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
private val viewModel: DetailViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
private val binding by viewBinding(ActivityDetailBinding::inflate)
|
||||||
|
|
||||||
|
private lateinit var statusAdapter: DetailStatusAdapter
|
||||||
|
|
||||||
|
private lateinit var repliesAdapter: DetailReplyAdapter
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||||
|
val top = insets.systemWindowInsetTop
|
||||||
|
|
||||||
|
binding.root.setPadding(0, top, 0, 0)
|
||||||
|
|
||||||
|
insets.consumeSystemWindowInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.setStatusId(intent.getStringExtra(EXTRA_STATUS_ID)!!)
|
||||||
|
|
||||||
|
val displayWidth = getDisplayWidthInPx()
|
||||||
|
|
||||||
|
statusAdapter = DetailStatusAdapter(displayWidth, this)
|
||||||
|
repliesAdapter = DetailReplyAdapter(this)
|
||||||
|
|
||||||
|
binding.detailRecyclerView.adapter = MergeAdapter(statusAdapter, repliesAdapter)
|
||||||
|
|
||||||
|
viewModel.currentStatus.observe(this, Observer {
|
||||||
|
if(it is Success) {
|
||||||
|
binding.detailProgress.hide()
|
||||||
|
binding.detailRecyclerView.show()
|
||||||
|
statusAdapter.submitList(listOf(it.data))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.replies.observe(this, Observer {
|
||||||
|
if(it is Success) {
|
||||||
|
repliesAdapter.submitList(it.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFavorite(post: StatusEntity) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBoost(post: StatusEntity) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReply(status: StatusEntity) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaVisibilityChanged(status: StatusEntity) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetailsOpened(status: StatusEntity) {
|
||||||
|
// nothing to do, we already are in details
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_STATUS_ID = "STATUS_ID"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, statusId: String): Intent {
|
||||||
|
return Intent(context, DetailActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_STATUS_ID, statusId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package at.connyduck.pixelcat.components.timeline.detail
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.text.parseAsHtml
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import at.connyduck.pixelcat.components.timeline.TimeLineActionListener
|
||||||
|
import at.connyduck.pixelcat.components.timeline.TimelineDiffUtil
|
||||||
|
import at.connyduck.pixelcat.components.util.BindingHolder
|
||||||
|
import at.connyduck.pixelcat.databinding.ItemReplyBinding
|
||||||
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
|
import coil.api.load
|
||||||
|
import coil.transform.RoundedCornersTransformation
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
class DetailReplyAdapter(
|
||||||
|
private val listener: TimeLineActionListener
|
||||||
|
): ListAdapter<StatusEntity, BindingHolder<ItemReplyBinding>>(TimelineDiffUtil) {
|
||||||
|
|
||||||
|
private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemReplyBinding> {
|
||||||
|
val binding = ItemReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return BindingHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingHolder<ItemReplyBinding>, position: Int) {
|
||||||
|
getItem(position)?.let { status ->
|
||||||
|
|
||||||
|
holder.binding.postAvatar.load(status.account.avatar) {
|
||||||
|
transformations(RoundedCornersTransformation(25f))
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.postDisplayName.text = status.account.displayName
|
||||||
|
holder.binding.postName.text = "@${status.account.username}"
|
||||||
|
|
||||||
|
holder.binding.postDescription.text = status.content.parseAsHtml().trim()
|
||||||
|
|
||||||
|
holder.binding.postDate.text = dateTimeFormatter.format(status.createdAt)
|
||||||
|
|
||||||
|
holder.binding.postLikeButton.isChecked = status.favourited
|
||||||
|
|
||||||
|
holder.binding.postLikeButton.setEventListener { _, _ ->
|
||||||
|
listener.onFavorite(status)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.postReplyButton.setOnClickListener {
|
||||||
|
listener.onReply(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package at.connyduck.pixelcat.components.timeline.detail
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import at.connyduck.pixelcat.components.timeline.TimeLineActionListener
|
||||||
|
import at.connyduck.pixelcat.components.timeline.TimelineDiffUtil
|
||||||
|
import at.connyduck.pixelcat.components.timeline.TimelineImageAdapter
|
||||||
|
import at.connyduck.pixelcat.components.timeline.bind
|
||||||
|
import at.connyduck.pixelcat.components.util.BindingHolder
|
||||||
|
import at.connyduck.pixelcat.databinding.ItemStatusBinding
|
||||||
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
|
||||||
|
class DetailStatusAdapter(
|
||||||
|
private val displayWidth: Int,
|
||||||
|
private val listener: TimeLineActionListener
|
||||||
|
): ListAdapter<StatusEntity, BindingHolder<ItemStatusBinding>>(TimelineDiffUtil) {
|
||||||
|
|
||||||
|
private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemStatusBinding> {
|
||||||
|
val binding = ItemStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
binding.postImages.adapter = TimelineImageAdapter()
|
||||||
|
binding.postIndicator.setViewPager(binding.postImages)
|
||||||
|
(binding.postImages.adapter as TimelineImageAdapter).registerAdapterDataObserver(binding.postIndicator.adapterDataObserver)
|
||||||
|
return BindingHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingHolder<ItemStatusBinding>, position: Int) {
|
||||||
|
getItem(position)?.let { status ->
|
||||||
|
holder.bind(status, displayWidth, listener, dateTimeFormatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Conny Duck
|
||||||
|
*
|
||||||
|
* This file is part of Pixelcat.
|
||||||
|
*
|
||||||
|
* Pixelcat 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.
|
||||||
|
*
|
||||||
|
* Pixelcat 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package at.connyduck.pixelcat.components.timeline.detail
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.connyduck.pixelcat.components.util.Loading
|
||||||
|
import at.connyduck.pixelcat.components.util.Success
|
||||||
|
import at.connyduck.pixelcat.components.util.UiState
|
||||||
|
import at.connyduck.pixelcat.components.util.Error
|
||||||
|
import at.connyduck.pixelcat.db.AccountManager
|
||||||
|
import at.connyduck.pixelcat.db.AppDatabase
|
||||||
|
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||||
|
import at.connyduck.pixelcat.db.entitity.toEntity
|
||||||
|
import at.connyduck.pixelcat.network.FediverseApi
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DetailViewModel @Inject constructor(
|
||||||
|
val api: FediverseApi,
|
||||||
|
val db: AppDatabase,
|
||||||
|
val accountManager: AccountManager
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val currentStatus = MutableLiveData<UiState<StatusEntity>>()
|
||||||
|
val replies = MutableLiveData<UiState<List<StatusEntity>>>()
|
||||||
|
|
||||||
|
private var statusId = ""
|
||||||
|
|
||||||
|
fun setStatusId(statusId: String) {
|
||||||
|
this.statusId = statusId
|
||||||
|
|
||||||
|
currentStatus.value = Loading()
|
||||||
|
replies.value = Loading()
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
db.statusDao().status(statusId, accountManager.activeAccount()?.id!!)?.let {
|
||||||
|
currentStatus.value = Success(it)
|
||||||
|
}
|
||||||
|
loadStatus()
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadReplies()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
currentStatus.value = Loading()
|
||||||
|
replies.value = Loading()
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadStatus()
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadReplies()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun loadStatus() {
|
||||||
|
api.status(statusId).fold({
|
||||||
|
val statusEntity = it.toEntity(accountManager.activeAccount()?.id!!)
|
||||||
|
db.statusDao().insertOrReplace(statusEntity)
|
||||||
|
currentStatus.value = Success(statusEntity)
|
||||||
|
}, {
|
||||||
|
currentStatus.value = Error(cause = it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadReplies() {
|
||||||
|
api.statusContext(statusId).fold({
|
||||||
|
replies.value = Success(it.descendants.map{
|
||||||
|
descendant -> descendant.toEntity(accountManager.activeAccount()?.id!!)
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
replies.value = Error(cause = it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import at.connyduck.pixelcat.components.main.MainActivity
|
||||||
import at.connyduck.pixelcat.components.about.AboutActivity
|
import at.connyduck.pixelcat.components.about.AboutActivity
|
||||||
import at.connyduck.pixelcat.components.about.licenses.LicenseActivity
|
import at.connyduck.pixelcat.components.about.licenses.LicenseActivity
|
||||||
import at.connyduck.pixelcat.components.compose.ComposeActivity
|
import at.connyduck.pixelcat.components.compose.ComposeActivity
|
||||||
|
import at.connyduck.pixelcat.components.timeline.detail.DetailActivity
|
||||||
import at.connyduck.pixelcat.components.login.LoginActivity
|
import at.connyduck.pixelcat.components.login.LoginActivity
|
||||||
import at.connyduck.pixelcat.components.profile.ProfileActivity
|
import at.connyduck.pixelcat.components.profile.ProfileActivity
|
||||||
import at.connyduck.pixelcat.components.settings.SettingsActivity
|
import at.connyduck.pixelcat.components.settings.SettingsActivity
|
||||||
|
@ -57,4 +58,7 @@ abstract class ActivityModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesComposeActivity(): ComposeActivity
|
abstract fun contributesComposeActivity(): ComposeActivity
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesDetailActivity(): DetailActivity
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,12 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// from https://proandroiddev.com/viewmodel-with-dagger2-architecture-components-2e06f06c9455
|
|
||||||
|
|
||||||
package at.connyduck.pixelcat.dagger
|
package at.connyduck.pixelcat.dagger
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import at.connyduck.pixelcat.components.compose.ComposeViewModel
|
import at.connyduck.pixelcat.components.compose.ComposeViewModel
|
||||||
|
import at.connyduck.pixelcat.components.timeline.detail.DetailViewModel
|
||||||
import at.connyduck.pixelcat.components.login.LoginViewModel
|
import at.connyduck.pixelcat.components.login.LoginViewModel
|
||||||
import at.connyduck.pixelcat.components.main.MainViewModel
|
import at.connyduck.pixelcat.components.main.MainViewModel
|
||||||
import at.connyduck.pixelcat.components.notifications.NotificationsViewModel
|
import at.connyduck.pixelcat.components.notifications.NotificationsViewModel
|
||||||
|
@ -90,5 +89,10 @@ abstract class ViewModelModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(ComposeViewModel::class)
|
@ViewModelKey(ComposeViewModel::class)
|
||||||
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
|
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(DetailViewModel::class)
|
||||||
|
internal abstract fun detailViewModel(viewModel: DetailViewModel): ViewModel
|
||||||
// Add more ViewModels here
|
// Add more ViewModels here
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@ interface TimelineDao {
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(status: StatusEntity)
|
suspend fun delete(status: StatusEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM StatusEntity WHERE id = :statusId AND accountId = :accountId")
|
||||||
|
suspend fun status(statusId: String, accountId: Long): StatusEntity?
|
||||||
|
|
||||||
@Query("SELECT * FROM StatusEntity WHERE accountId = :accountId ORDER BY LENGTH(id) DESC, id DESC")
|
@Query("SELECT * FROM StatusEntity WHERE accountId = :accountId ORDER BY LENGTH(id) DESC, id DESC")
|
||||||
fun statuses(accountId: Long): PagingSource<Int, StatusEntity>
|
fun statuses(accountId: Long): PagingSource<Int, StatusEntity>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package at.connyduck.pixelcat.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class StatusContext (
|
||||||
|
val ancestors: List<Status>,
|
||||||
|
val descendants: List<Status>
|
||||||
|
)
|
|
@ -19,13 +19,7 @@
|
||||||
|
|
||||||
package at.connyduck.pixelcat.network
|
package at.connyduck.pixelcat.network
|
||||||
|
|
||||||
import at.connyduck.pixelcat.model.AccessToken
|
import at.connyduck.pixelcat.model.*
|
||||||
import at.connyduck.pixelcat.model.Account
|
|
||||||
import at.connyduck.pixelcat.model.AppCredentials
|
|
||||||
import at.connyduck.pixelcat.model.Attachment
|
|
||||||
import at.connyduck.pixelcat.model.NewStatus
|
|
||||||
import at.connyduck.pixelcat.model.Relationship
|
|
||||||
import at.connyduck.pixelcat.model.Status
|
|
||||||
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
|
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
|
@ -183,4 +177,14 @@ interface FediverseApi {
|
||||||
suspend fun unreblogStatus(
|
suspend fun unreblogStatus(
|
||||||
@Path("id") statusId: String
|
@Path("id") statusId: String
|
||||||
): NetworkResponse<Status>
|
): NetworkResponse<Status>
|
||||||
|
|
||||||
|
@GET("api/v1/statuses/{id}")
|
||||||
|
suspend fun status(
|
||||||
|
@Path("id") statusId: String
|
||||||
|
): NetworkResponse<Status>
|
||||||
|
|
||||||
|
@GET("api/v1/statuses/{id}/context")
|
||||||
|
suspend fun statusContext(
|
||||||
|
@Path("id") statusId: String
|
||||||
|
): NetworkResponse<StatusContext>
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/pixelcat_gradient">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/detailAppbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/detailToolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways"
|
||||||
|
app:navigationIcon="@drawable/ic_cat_small"
|
||||||
|
app:title="@string/app_name"
|
||||||
|
app:titleTextAppearance="@style/TextAppearanceToolbar"
|
||||||
|
app:titleTextColor="?attr/colorPrimary" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/detailSwipeRefresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?android:attr/windowBackground"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/detailRecyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/detailProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/postAvatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:background="#f00" />
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/postDisplayName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/postName"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/postAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Conny Duck" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/postName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/postDisplayName"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="\@connyduck\@chaos.social" />
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/postDescription"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/postAvatar"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/postDisplayName"
|
||||||
|
tools:text="This is the caption" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/postDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/postAvatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/postDescription"
|
||||||
|
tools:text="4 hours ago" />
|
||||||
|
|
||||||
|
<at.connyduck.sparkbutton.SparkButton
|
||||||
|
android:id="@+id/postLikeButton"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/postDate"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/postDescription"
|
||||||
|
sparkbutton:activeImage="@drawable/ic_heart_filled"
|
||||||
|
sparkbutton:iconSize="24dp"
|
||||||
|
sparkbutton:inactiveImage="@drawable/ic_heart"
|
||||||
|
sparkbutton:primaryColor="@color/heart_button_primary"
|
||||||
|
sparkbutton:secondaryColor="@color/heart_button_secondary" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/postReplyButton"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_message"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/postLikeButton"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/postDescription" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue