Status details
* add status details * add pull to refresh * add like/boost functionality * add error handling * fix formatting * add reply BottomSheet * make replying work * make "replying to" string translateable
This commit is contained in:
parent
8317470c8e
commit
ea0bbffbc5
|
@ -48,6 +48,8 @@
|
|||
<activity android:name=".components.about.licenses.LicenseActivity"/>
|
||||
<activity android:name="at.connyduck.pixelcat.components.compose.ComposeActivity" />
|
||||
<activity android:name=".components.profile.ProfileActivity" />
|
||||
<activity android:name=".components.timeline.detail.DetailActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateVisible"/>
|
||||
|
||||
<service android:name=".components.compose.SendStatusService" />
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import androidx.paging.LoadState
|
|||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
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.getColorForAttr
|
||||
import at.connyduck.pixelcat.dagger.ViewModelFactory
|
||||
|
@ -79,29 +80,31 @@ class TimelineFragment : DaggerFragment(R.layout.fragment_timeline), TimeLineAct
|
|||
adapter.addLoadStateListener {
|
||||
if (it.refresh != LoadState.Loading) {
|
||||
binding.timelineSwipeRefresh.isRefreshing = false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = TimelineFragment()
|
||||
override fun onFavorite(status: StatusEntity) {
|
||||
viewModel.onFavorite(status)
|
||||
}
|
||||
|
||||
override fun onFavorite(post: StatusEntity) {
|
||||
viewModel.onFavorite(post)
|
||||
}
|
||||
|
||||
override fun onBoost(post: StatusEntity) {
|
||||
viewModel.onBoost(post)
|
||||
override fun onBoost(status: StatusEntity) {
|
||||
viewModel.onBoost(status)
|
||||
}
|
||||
|
||||
override fun onReply(status: StatusEntity) {
|
||||
TODO("Not yet implemented")
|
||||
startActivity(DetailActivity.newIntent(requireContext(), status.actionableId, reply = true))
|
||||
}
|
||||
|
||||
override fun onMediaVisibilityChanged(status: StatusEntity) {
|
||||
viewModel.onMediaVisibilityChanged(status)
|
||||
}
|
||||
|
||||
override fun onDetailsOpened(status: StatusEntity) {
|
||||
startActivity(DetailActivity.newIntent(requireContext(), status.actionableId))
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = TimelineFragment()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,13 +31,15 @@ import at.connyduck.pixelcat.databinding.ItemStatusBinding
|
|||
import at.connyduck.pixelcat.db.entitity.StatusEntity
|
||||
import coil.api.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
interface TimeLineActionListener {
|
||||
fun onFavorite(post: StatusEntity)
|
||||
fun onBoost(post: StatusEntity)
|
||||
fun onFavorite(status: StatusEntity)
|
||||
fun onBoost(status: StatusEntity)
|
||||
fun onReply(status: StatusEntity)
|
||||
fun onMediaVisibilityChanged(status: StatusEntity)
|
||||
fun onDetailsOpened(status: StatusEntity)
|
||||
}
|
||||
|
||||
object TimelineDiffUtil : DiffUtil.ItemCallback<StatusEntity>() {
|
||||
|
@ -66,65 +68,71 @@ class TimelineListAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemStatusBinding>, position: Int) {
|
||||
|
||||
getItem(position)?.let { status ->
|
||||
|
||||
// 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()
|
||||
}
|
||||
}.maxOrNull()?.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)
|
||||
}
|
||||
holder.bind(status, displayWidth, listener, dateTimeFormatter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,58 @@
|
|||
package at.connyduck.pixelcat.components.timeline
|
||||
|
||||
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.model.Status
|
||||
import at.connyduck.pixelcat.network.FediverseApi
|
||||
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class TimelineUseCases @Inject constructor(
|
||||
val api: FediverseApi,
|
||||
val db: AppDatabase,
|
||||
val accountManager: AccountManager
|
||||
) {
|
||||
|
||||
suspend fun onFavorite(status: StatusEntity) {
|
||||
val alreadyFavourited = status.favourited
|
||||
if (alreadyFavourited) {
|
||||
api.unfavouriteStatus(status.actionableId)
|
||||
} else {
|
||||
api.favouriteStatus(status.actionableId)
|
||||
}.updateStatusInDb()
|
||||
}
|
||||
|
||||
suspend fun onBoost(status: StatusEntity) {
|
||||
val alreadyBoosted = status.reblogged
|
||||
if (alreadyBoosted) {
|
||||
api.unreblogStatus(status.actionableId)
|
||||
} else {
|
||||
api.reblogStatus(status.actionableId)
|
||||
}.updateStatusInDb()
|
||||
}
|
||||
|
||||
suspend fun onMediaVisibilityChanged(status: StatusEntity) {
|
||||
db.statusDao().changeMediaVisibility(
|
||||
!status.mediaVisible,
|
||||
status.id,
|
||||
accountManager.activeAccount()?.id!!
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun NetworkResponse<Status>.updateStatusInDb() {
|
||||
fold<Any?>(
|
||||
{ updatedStatus ->
|
||||
val accountId = accountManager.activeAccount()?.id!!
|
||||
val updatedStatusEntity = updatedStatus.toEntity(accountId)
|
||||
db.statusDao().insertOrReplace(updatedStatusEntity)
|
||||
},
|
||||
{
|
||||
// Todo
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -28,10 +28,7 @@ import androidx.paging.cachedIn
|
|||
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.model.Status
|
||||
import at.connyduck.pixelcat.network.FediverseApi
|
||||
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
|
@ -39,9 +36,10 @@ import kotlinx.coroutines.launch
|
|||
import javax.inject.Inject
|
||||
|
||||
class TimelineViewModel @Inject constructor(
|
||||
private val accountManager: AccountManager,
|
||||
accountManager: AccountManager,
|
||||
private val db: AppDatabase,
|
||||
private val fediverseApi: FediverseApi
|
||||
private val fediverseApi: FediverseApi,
|
||||
private val useCases: TimelineUseCases
|
||||
) : ViewModel() {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
|
@ -58,46 +56,19 @@ class TimelineViewModel @Inject constructor(
|
|||
|
||||
fun onFavorite(status: StatusEntity) {
|
||||
viewModelScope.launch {
|
||||
val alreadyFavourited = status.favourited
|
||||
if (alreadyFavourited) {
|
||||
fediverseApi.unfavouriteStatus(status.actionableId)
|
||||
} else {
|
||||
fediverseApi.favouriteStatus(status.actionableId)
|
||||
}.updateStatusInDb()
|
||||
useCases.onFavorite(status)
|
||||
}
|
||||
}
|
||||
|
||||
fun onBoost(status: StatusEntity) {
|
||||
viewModelScope.launch {
|
||||
val alreadyBoosted = status.reblogged
|
||||
if (alreadyBoosted) {
|
||||
fediverseApi.unreblogStatus(status.actionableId)
|
||||
} else {
|
||||
fediverseApi.reblogStatus(status.actionableId)
|
||||
}.updateStatusInDb()
|
||||
useCases.onBoost(status)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMediaVisibilityChanged(status: StatusEntity) {
|
||||
viewModelScope.launch {
|
||||
db.statusDao().changeMediaVisibility(
|
||||
!status.mediaVisible,
|
||||
status.id,
|
||||
accountManager.activeAccount()?.id!!
|
||||
)
|
||||
useCases.onMediaVisibilityChanged(status)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun NetworkResponse<Status>.updateStatusInDb() {
|
||||
fold<Any?>(
|
||||
{ updatedStatus ->
|
||||
val accountId = accountManager.activeAccount()?.id!!
|
||||
val updatedStatusEntity = updatedStatus.toEntity(accountId)
|
||||
db.statusDao().insertOrReplace(updatedStatusEntity)
|
||||
},
|
||||
{
|
||||
// Todo
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
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.core.graphics.Insets
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import at.connyduck.pixelcat.R
|
||||
import at.connyduck.pixelcat.components.general.BaseActivity
|
||||
import at.connyduck.pixelcat.components.timeline.TimeLineActionListener
|
||||
import at.connyduck.pixelcat.components.util.Error
|
||||
import at.connyduck.pixelcat.components.util.Loading
|
||||
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.components.util.getColorForAttr
|
||||
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 com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
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 ->
|
||||
binding.root.setPadding(0, insets.systemWindowInsetTop, 0, 0)
|
||||
|
||||
WindowInsetsCompat.Builder(WindowInsetsCompat.toWindowInsetsCompat(insets))
|
||||
.setSystemWindowInsets(Insets.of(insets.systemWindowInsetLeft, 0, insets.systemWindowInsetRight, insets.systemWindowInsetBottom))
|
||||
.build()
|
||||
.toWindowInsets()
|
||||
}
|
||||
|
||||
binding.detailSwipeRefresh.setColorSchemeColors(
|
||||
getColorForAttr(R.attr.pixelcat_gradient_color_start),
|
||||
getColorForAttr(R.attr.pixelcat_gradient_color_end)
|
||||
)
|
||||
|
||||
binding.detailSwipeRefresh.setOnRefreshListener {
|
||||
viewModel.reload(false)
|
||||
}
|
||||
|
||||
binding.detailStatus.setOnRetryListener {
|
||||
viewModel.reload(true)
|
||||
}
|
||||
|
||||
viewModel.setStatusId(intent.getStringExtra(EXTRA_STATUS_ID)!!)
|
||||
|
||||
val displayWidth = getDisplayWidthInPx()
|
||||
|
||||
statusAdapter = DetailStatusAdapter(displayWidth, this)
|
||||
repliesAdapter = DetailReplyAdapter(this)
|
||||
|
||||
binding.detailRecyclerView.adapter = ConcatAdapter(statusAdapter, repliesAdapter)
|
||||
|
||||
viewModel.currentStatus.observe(
|
||||
this,
|
||||
{
|
||||
when (it) {
|
||||
is Success -> {
|
||||
binding.detailSwipeRefresh.show()
|
||||
binding.detailStatus.hide()
|
||||
binding.detailProgress.hide()
|
||||
binding.detailSwipeRefresh.isRefreshing = false
|
||||
binding.detailRecyclerView.show()
|
||||
statusAdapter.submitList(listOf(it.data))
|
||||
it.data?.let { status ->
|
||||
if (intent.getBooleanExtra(EXTRA_REPLY, false)) {
|
||||
intent.removeExtra(EXTRA_REPLY)
|
||||
onReply(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Loading -> {
|
||||
binding.detailSwipeRefresh.hide()
|
||||
binding.detailStatus.hide()
|
||||
binding.detailProgress.show()
|
||||
}
|
||||
is Error -> {
|
||||
binding.detailSwipeRefresh.hide()
|
||||
binding.detailStatus.show()
|
||||
binding.detailProgress.hide()
|
||||
binding.detailStatus.showGeneralError()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
viewModel.replies.observe(
|
||||
this,
|
||||
{
|
||||
if (it is Success) {
|
||||
repliesAdapter.submitList(it.data)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFavorite(status: StatusEntity) {
|
||||
viewModel.onFavorite(status)
|
||||
}
|
||||
|
||||
override fun onBoost(status: StatusEntity) {
|
||||
viewModel.onBoost(status)
|
||||
}
|
||||
|
||||
override fun onReply(status: StatusEntity) {
|
||||
val replyBottomsheet = BottomSheetBehavior.from(binding.detailReplyBottomSheet)
|
||||
replyBottomsheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
||||
binding.detailReplyingTo.text = getString(R.string.status_details_replying_to, "@" + status.account.username)
|
||||
binding.detailReply.requestFocus()
|
||||
binding.detailReply.setText("@" + status.account.username + " ")
|
||||
binding.detailReply.setSelection(binding.detailReply.text?.length ?: 0)
|
||||
|
||||
binding.detailReplyLayout.setEndIconOnClickListener {
|
||||
viewModel.onReply(status, binding.detailReply.text?.toString().orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaVisibilityChanged(status: StatusEntity) {
|
||||
viewModel.onMediaVisibilityChanged(status)
|
||||
}
|
||||
|
||||
override fun onDetailsOpened(status: StatusEntity) {
|
||||
// nothing to do, we already are in details
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_STATUS_ID = "STATUS_ID"
|
||||
private const val EXTRA_REPLY = "REPLY"
|
||||
|
||||
fun newIntent(context: Context, statusId: String, reply: Boolean = false): Intent {
|
||||
return Intent(context, DetailActivity::class.java).apply {
|
||||
putExtra(EXTRA_STATUS_ID, statusId)
|
||||
putExtra(EXTRA_REPLY, reply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
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,38 @@
|
|||
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,143 @@
|
|||
/*
|
||||
* 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.timeline.TimelineUseCases
|
||||
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.model.NewStatus
|
||||
import at.connyduck.pixelcat.network.FediverseApi
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class DetailViewModel @Inject constructor(
|
||||
private val api: FediverseApi,
|
||||
private val db: AppDatabase,
|
||||
private val accountManager: AccountManager,
|
||||
private val useCases: TimelineUseCases,
|
||||
private val fediverseApi: FediverseApi
|
||||
) : 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(showLoading: Boolean) {
|
||||
if (showLoading) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun onFavorite(status: StatusEntity) {
|
||||
viewModelScope.launch {
|
||||
useCases.onFavorite(status)
|
||||
}
|
||||
}
|
||||
|
||||
fun onBoost(status: StatusEntity) {
|
||||
viewModelScope.launch {
|
||||
useCases.onBoost(status)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMediaVisibilityChanged(status: StatusEntity) {
|
||||
viewModelScope.launch {
|
||||
useCases.onMediaVisibilityChanged(status)
|
||||
}
|
||||
}
|
||||
|
||||
fun onReply(statusToReply: StatusEntity, replyText: String) {
|
||||
viewModelScope.launch {
|
||||
|
||||
fediverseApi.reply(
|
||||
NewStatus(
|
||||
status = replyText,
|
||||
inReplyToId = statusToReply.actionableId,
|
||||
visibility = statusToReply.visibility.name.toLowerCase(Locale.ROOT),
|
||||
sensitive = statusToReply.sensitive,
|
||||
mediaIds = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import at.connyduck.pixelcat.R
|
||||
import at.connyduck.pixelcat.components.util.extension.getColorForAttr
|
||||
import at.connyduck.pixelcat.databinding.ViewStatusBinding
|
||||
|
||||
class StatusView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
val binding: ViewStatusBinding
|
||||
|
||||
init {
|
||||
binding = ViewStatusBinding.inflate(LayoutInflater.from(context), this)
|
||||
gravity = Gravity.CENTER
|
||||
setBackgroundColor(context.getColorForAttr(R.attr.colorSurface))
|
||||
orientation = VERTICAL
|
||||
}
|
||||
|
||||
fun setOnRetryListener(listener: (View) -> Unit) {
|
||||
binding.statusButton.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun showGeneralError() {
|
||||
binding.statusMessage.setText(R.string.status_general_error)
|
||||
binding.statusMessage.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_alert_triangle, 0, 0)
|
||||
}
|
||||
|
||||
fun showNetworkError() {
|
||||
binding.statusMessage.setText(R.string.status_network_error)
|
||||
binding.statusMessage.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_wifi_off, 0, 0)
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import at.connyduck.pixelcat.components.main.MainActivity
|
|||
import at.connyduck.pixelcat.components.about.AboutActivity
|
||||
import at.connyduck.pixelcat.components.about.licenses.LicenseActivity
|
||||
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.profile.ProfileActivity
|
||||
import at.connyduck.pixelcat.components.settings.SettingsActivity
|
||||
|
@ -57,4 +58,7 @@ abstract class ActivityModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesComposeActivity(): ComposeActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesDetailActivity(): DetailActivity
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ class NetworkModule {
|
|||
if (BuildConfig.DEBUG) {
|
||||
okHttpClientBuilder.addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,13 +17,12 @@
|
|||
* 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
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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.main.MainViewModel
|
||||
import at.connyduck.pixelcat.components.notifications.NotificationsViewModel
|
||||
|
@ -90,5 +89,10 @@ abstract class ViewModelModule {
|
|||
@IntoMap
|
||||
@ViewModelKey(ComposeViewModel::class)
|
||||
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(DetailViewModel::class)
|
||||
internal abstract fun detailViewModel(viewModel: DetailViewModel): ViewModel
|
||||
// Add more ViewModels here
|
||||
}
|
||||
|
|
|
@ -39,6 +39,9 @@ interface TimelineDao {
|
|||
@Delete
|
||||
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")
|
||||
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>
|
||||
)
|
|
@ -26,6 +26,7 @@ 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.model.StatusContext
|
||||
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
|
||||
import okhttp3.MultipartBody
|
||||
import retrofit2.http.Body
|
||||
|
@ -164,6 +165,11 @@ interface FediverseApi {
|
|||
@Body status: NewStatus
|
||||
): NetworkResponse<Status>
|
||||
|
||||
@POST("api/v1/statuses")
|
||||
suspend fun reply(
|
||||
@Body status: NewStatus
|
||||
): NetworkResponse<Status>
|
||||
|
||||
@POST("api/v1/statuses/{id}/favourite")
|
||||
suspend fun favouriteStatus(
|
||||
@Path("id") statusId: String
|
||||
|
@ -183,4 +189,14 @@ interface FediverseApi {
|
|||
suspend fun unreblogStatus(
|
||||
@Path("id") statusId: String
|
||||
): 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,14 @@
|
|||
<vector android:height="96dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M10.29,3.86L1.82,18a2,2 0,0 0,1.71 3h16.94a2,2 0,0 0,1.71 -3L13.71,3.86a2,2 0,0 0,-3.42 0z"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M12,9L12,13"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M12,17L12.01,17"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22,2L11,13"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?android:attr/textColorTertiary"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M22,2l-7,20l-4,-9l-9,-4l20,-7z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="?android:attr/textColorTertiary"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,30 @@
|
|||
<vector android:height="96dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000" android:pathData="M1,1L23,23"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M16.72,11.06A10.94,10.94 0,0 1,19 12.55"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M5,12.55a10.94,10.94 0,0 1,5.17 -2.39"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M10.71,5.05A16,16 0,0 1,22.58 9"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M1.42,9a15.91,15.91 0,0 1,4.7 -2.88"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M8.53,16.11a6,6 0,0 1,6.95 0"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M12,20L12.01,20"
|
||||
android:strokeColor="?android:attr/textColorTertiary" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
|
@ -0,0 +1,104 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/pixelcat_gradient"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<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>
|
||||
|
||||
<at.connyduck.pixelcat.components.view.StatusView
|
||||
android:id="@+id/detailStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/detailProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/detailReplyBottomSheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_bottom_sheet"
|
||||
android:elevation="4dp"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/detailReplyingTo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="replying to @ConnyDuck" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/detailReplyLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:endIconDrawable="@drawable/ic_send"
|
||||
app:endIconMode="custom"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/detailReplyingTo">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/detailReply"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</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>
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:background="?attr/colorSurface"
|
||||
tools:gravity="center"
|
||||
tools:orientation="vertical"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:drawablePadding="16dp"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textAlignment="center"
|
||||
tools:drawableTop="@drawable/ic_wifi_off"
|
||||
tools:text="An unexpected error occured" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/statusButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/action_retry" />
|
||||
|
||||
</merge>
|
|
@ -76,4 +76,10 @@
|
|||
<string name="sensitive_media">Sensitive media</string>
|
||||
<string name="about_license_description">Pixelcat is free and open-source software. It is licensed under the GNU General Public License Version 3.</string>
|
||||
|
||||
<string name="status_general_error">An unexpected error occurred</string>
|
||||
<string name="status_network_error">Failed to connect. Please check your internet connection-</string>
|
||||
|
||||
<string name="status_details_replying_to">Replying to $1%s</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue