diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 362d915..6d9c63f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -48,6 +48,8 @@
+
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineFragment.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineFragment.kt
index fdd5706..8d1bd04 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineFragment.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineFragment.kt
@@ -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()
+ }
}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineListAdapter.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineListAdapter.kt
index 850f647..71471d6 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineListAdapter.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineListAdapter.kt
@@ -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() {
@@ -66,65 +68,71 @@ class TimelineListAdapter(
}
override fun onBindViewHolder(holder: BindingHolder, 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.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)
+ }
+}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineUseCases.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineUseCases.kt
new file mode 100644
index 0000000..1eae3a3
--- /dev/null
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineUseCases.kt
@@ -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.updateStatusInDb() {
+ fold(
+ { updatedStatus ->
+ val accountId = accountManager.activeAccount()?.id!!
+ val updatedStatusEntity = updatedStatus.toEntity(accountId)
+ db.statusDao().insertOrReplace(updatedStatusEntity)
+ },
+ {
+ // Todo
+ }
+ )
+ }
+}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineViewModel.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineViewModel.kt
index 9449a85..3e604b9 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineViewModel.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/TimelineViewModel.kt
@@ -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.updateStatusInDb() {
- fold(
- { updatedStatus ->
- val accountId = accountManager.activeAccount()?.id!!
- val updatedStatusEntity = updatedStatus.toEntity(accountId)
- db.statusDao().insertOrReplace(updatedStatusEntity)
- },
- {
- // Todo
- }
- )
- }
}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailActivity.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailActivity.kt
new file mode 100644
index 0000000..9af28ae
--- /dev/null
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailActivity.kt
@@ -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)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailReplyAdapter.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailReplyAdapter.kt
new file mode 100644
index 0000000..5242af9
--- /dev/null
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailReplyAdapter.kt
@@ -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>(TimelineDiffUtil) {
+
+ private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): BindingHolder {
+ val binding = ItemReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return BindingHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: BindingHolder, 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)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailStatusAdapter.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailStatusAdapter.kt
new file mode 100644
index 0000000..9d2482c
--- /dev/null
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailStatusAdapter.kt
@@ -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>(TimelineDiffUtil) {
+
+ private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): BindingHolder {
+ 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, position: Int) {
+ getItem(position)?.let { status ->
+ holder.bind(status, displayWidth, listener, dateTimeFormatter)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailViewModel.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailViewModel.kt
new file mode 100644
index 0000000..62b0879
--- /dev/null
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/timeline/detail/DetailViewModel.kt
@@ -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 .
+ */
+
+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>()
+ val replies = MutableLiveData>>()
+
+ 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
+ )
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/view/StatusView.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/view/StatusView.kt
new file mode 100644
index 0000000..ff7a679
--- /dev/null
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/view/StatusView.kt
@@ -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 .
+ */
+
+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)
+ }
+}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ActivityModule.kt b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ActivityModule.kt
index 1a54c30..bdac11a 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ActivityModule.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ActivityModule.kt
@@ -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
}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt
index 3d39a2f..3a12c3a 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt
@@ -56,7 +56,7 @@ class NetworkModule {
if (BuildConfig.DEBUG) {
okHttpClientBuilder.addInterceptor(
HttpLoggingInterceptor().apply {
- level = HttpLoggingInterceptor.Level.BASIC
+ level = HttpLoggingInterceptor.Level.BODY
}
)
}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ViewModelFactory.kt b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ViewModelFactory.kt
index d9c2b4f..9a0f565 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ViewModelFactory.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/ViewModelFactory.kt
@@ -17,13 +17,12 @@
* along with this program. If not, see .
*/
-// 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
}
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/db/TimelineDao.kt b/app/src/main/kotlin/at/connyduck/pixelcat/db/TimelineDao.kt
index 9e64da8..ce4ca4f 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/db/TimelineDao.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/db/TimelineDao.kt
@@ -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
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/model/StatusContext.kt b/app/src/main/kotlin/at/connyduck/pixelcat/model/StatusContext.kt
new file mode 100644
index 0000000..0c09df8
--- /dev/null
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/model/StatusContext.kt
@@ -0,0 +1,9 @@
+package at.connyduck.pixelcat.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class StatusContext(
+ val ancestors: List,
+ val descendants: List
+)
diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt b/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt
index 1b5c42f..a7c64ba 100644
--- a/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt
+++ b/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt
@@ -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
+ @POST("api/v1/statuses")
+ suspend fun reply(
+ @Body status: NewStatus
+ ): NetworkResponse
+
@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
+
+ @GET("api/v1/statuses/{id}")
+ suspend fun status(
+ @Path("id") statusId: String
+ ): NetworkResponse
+
+ @GET("api/v1/statuses/{id}/context")
+ suspend fun statusContext(
+ @Path("id") statusId: String
+ ): NetworkResponse
}
diff --git a/app/src/main/res/drawable/ic_alert_triangle.xml b/app/src/main/res/drawable/ic_alert_triangle.xml
new file mode 100644
index 0000000..ac29b98
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alert_triangle.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_send.xml b/app/src/main/res/drawable/ic_send.xml
new file mode 100644
index 0000000..8c81c92
--- /dev/null
+++ b/app/src/main/res/drawable/ic_send.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_wifi_off.xml b/app/src/main/res/drawable/ic_wifi_off.xml
new file mode 100644
index 0000000..720b3ae
--- /dev/null
+++ b/app/src/main/res/drawable/ic_wifi_off.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml
new file mode 100644
index 0000000..5905606
--- /dev/null
+++ b/app/src/main/res/layout/activity_detail.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_reply.xml b/app/src/main/res/layout/item_reply.xml
new file mode 100644
index 0000000..c2c693d
--- /dev/null
+++ b/app/src/main/res/layout/item_reply.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_status.xml b/app/src/main/res/layout/view_status.xml
new file mode 100644
index 0000000..a18a348
--- /dev/null
+++ b/app/src/main/res/layout/view_status.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c942c7a..0a63e06 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -76,4 +76,10 @@
Sensitive media
Pixelcat is free and open-source software. It is licensed under the GNU General Public License Version 3.
+ An unexpected error occurred
+ Failed to connect. Please check your internet connection-
+
+ Replying to $1%s
+
+